Skip to content

ADR-026: First WRITE capability adapter semantic unit

  • Status: Accepted (2026-08-12, owner)
  • Date: 2026-08-11
  • Scope: Architecture and disposable-lab evidence design only. This ADR does not authorize an adapter, preparer, endpoint allow-list entry, live mutation, WRITE activation, MCP tool, or production construction.

Owner convergence decision

The owner accepted this description-only semantic unit and its semantic-scope-specific acceptance policy on 2026-08-12. Acceptance authorizes no implementation, capability/endpoint activation, MCP tool, production construction, or live execution. W1 remains separately authorization-gated.

Evidence is mandatory before first WRITE only when it protects behavior the exact semantic unit can exercise. Broader alias mutations, generic scenario machinery, and exhaustive permutations are not promoted into product prerequisites merely because they appear in the evidence catalogue below. Existing valid evidence is reused and unperformed deferred evidence is never reported as PASS.

First-WRITE acceptance matrix

Status meanings: ESTABLISHED is accepted existing evidence; MUST COMPLETE remains a W1/W2/W3 acceptance gate; DEFERRED is outside the first-WRITE semantic scope and is not PASS.

First-WRITE invariant Status at acceptance Accepted evidence / remaining requirement
exact target identity and singular resolution ESTABLISHED clean campaign and Stage 3A authoritative exact-name reads; production adapter retains zero/multiple refusal
complete authoritative pre-mutation READ ESTABLISHED name/type/description/ordered address/ordered detail captured authoritatively
complete protected fingerprint ESTABLISHED the exact five-field semantic fingerprint and ordering were exercised; production tests must preserve it unchanged
fresh lifecycle locator and continuity ESTABLISHED locator 0 remained stable in 25 clean cycles and all completed description cases; executor refusal on drift is already tested
omitted-field/protected-sibling preservation ESTABLISHED protected fields and ordering remained unchanged throughout clean and description evidence
explicit apply/reload suppression contract VERIFIED (2026-08-16) live first-WRITE evidence — see "Live first-WRITE evidence (2026-08-16)" below
deterministic authoritative postcondition ESTABLISHED 25/25 verified B reads plus exact normalization/boundary evidence
description-field concurrent-change/conflict refusal VERIFIED (offline, production-bound) (2026-08-16) tests/tier1/test_alias_description_execution.py::test_production_adapter_drift_refuses_before_executor_send drives the real AliasDescriptionAdapterV1 + MutationExecutor (not test_executor.py's generic _SyntheticAdapter) through a description change concurrent with a confirmed contract: zero sends, FAILED. See "ADR-026 acceptance-matrix closure evidence (2026-08-16)" below for the interpretation this reclassification rests on.
stale expected-state refusal VERIFIED (offline, production-bound) (2026-08-16) same test as above — the fingerprint drift is the staleness the executor detects before send, through the same real adapter path.
conflict-safe rollback from verified/reconciled B VERIFIED (offline, production-bound) (2026-08-16) test_production_adapter_rollback_conflict_refuses_and_post_expiry_recovery_remains_available drives a real VERIFIED contract through the real adapter, proves a concurrent post-B change refuses rollback (ROLLBACK_FAILED, zero sends), and separately proves a legitimate rollback succeeds and restores exact A after the authorization's own expiry (proving rollback authority is the sealed VERIFIED/B precondition, not authorization freshness).
exact authoritative A restoration ESTABLISHED 25/25 clean restorations and every accepted Stage 3A/3B case restored exact A
at-most-one-send ESTABLISHED clean live send accounting plus MutationExecutor/FaultProxy offline tests exercise the same send owner; production wiring must not add a send path
no blind retry after uncertainty VERIFIED (offline, production-bound) (2026-08-16) test_replay_is_refused_before_second_contract_or_handoff, test_all_preconsumption_failures_leave_auth_unconsumed_and_zero_handoff, and test_resume_prepared_never_consumes_or_creates_and_duplicate_resume_is_safe (all in test_alias_description_execution.py) plus test_restart_reconciles_interrupted_executing_contract (test_production_runtime.py, using the real build_production_runtime(), not a test double) together prove zero resend across preconsumption failure, duplicate resume, and a simulated restart mid-EXECUTING.
authoritative uncertainty classification VERIFIED (offline, production-bound) (2026-08-16) Was the one genuine gap this pass found: DEFINITELY_APPLIED/DEFINITELY_NOT_APPLIED/AMBIGUOUS classification was previously proven only against test_executor.py's generic _SyntheticAdapter, never the real alias adapter. Closed by a new test, test_production_adapter_send_timeout_reaches_reconciliation_not_failed_or_resend: a TransportTimeoutError on the real adapter's send lands in RECONCILIATION (never FAILED, which would license a resend), exactly one send attempted.
fail-closed reconciliation VERIFIED (offline, production-bound) (2026-08-16) test_reconciliation_requires_pinned_authority_and_wrong_signature_fails_closed (test_production_runtime.py) proves the fixed production pinned reconciliation authority rejects a wrong signature; test_restart_reconciles_interrupted_executing_contract proves the fixed production verifier/resume construction reaches RECONCILIATION, not a resend, through build_production_runtime() itself.
authenticated recovery across restart VERIFIED (offline, production-bound) (2026-08-16) Same test_restart_reconciles_interrupted_executing_contract: two independent build_production_runtime() calls against the same store file simulate a real process restart; the interrupted EXECUTING contract is reconciled, never resent, through the real fixed production construction (schema-v7, V2 provenance, no test-only wiring).
least privilege for exact endpoint/capability VERIFIED (2026-08-16) live-provisioned scoped identity pfsense_mcp_tier1_lab performed the entire production Tier1 restoration WRITE end-to-end holding only the four minimum privileges — see "Scoped-credential live-WRITE evidence (2026-08-16, restoration ceremony)" below
sufficient authoritative side-effect evidence VERIFIED (2026-08-16) live first-WRITE evidence — see "Live first-WRITE evidence (2026-08-16)" below; the config-history clause that kept this PARTIALLY VERIFIED is now closed

Concurrency boundary

Description or protected-fingerprint change between preparation and the forward/rollback precondition must fail closed. Unrelated-resource concurrency does not require exhaustive empirical orchestration: the executor authorizes from the selected alias's complete fingerprint and lifecycle locator, never a global config revision, and the adapter builds a request only for the freshly resolved exact alias locator. An unrelated change therefore cannot satisfy a stale target fingerprint, change the resolved locator without refusal, or redirect the request. If W1 review discovers shared appliance behavior that breaks this separation, that concrete case becomes mandatory again.

W1 implementation evidence

The separately authorized W1 implementation completed the production-inert parts of this matrix offline. The two-field request, exact-name preparer and production adapter now reuse the accepted five-field fingerprint and fresh numeric lifecycle locator. A second authoritative preparation before authorization consumption refuses description, protected-sibling, locator, configured-target or stable-appliance-identity drift. Focused tests also run the production adapter through MutationExecutor and prove zero forward send on stale A, zero rollback send when live B differs from the sealed verified B, one coordinator handoff maximum, and no retry right after consumption or handoff failure.

This does not relabel remaining empirical/runtime rows as PASS. W2 still owns fixed production construction, authenticated restart/reconciliation wiring and authority configuration. W3 still owns endpoint/capability/tool activation, selected remaining live evidence, and the operational side-effect decision.

Response-loss and timeout boundary

The mandatory property is one send maximum, no blind retry, fresh authoritative observation, the closed DEFINITELY_APPLIED / DEFINITELY_NOT_APPLIED / AMBIGUOUS classification, and fail-closed signed reconciliation where needed. Existing tests are acceptable where they execute the same MutationExecutor, store, observation, and reconciliation owners. Every live transport-fault permutation is not independently mandatory. New live evidence is required only for material behavior introduced by the fixed production composition.

Side-effect boundary

HTTP success alone never proves absence of side effects. First-WRITE evidence must establish deterministic authoritative configuration read-back, required apply suppression, and that correctness/recovery needs no broader state transition. Exhaustive filter/service/API/webConfigurator/config-history instrumentation is not automatically required when existing authoritative evidence and pinned endpoint behavior establish that contract. A genuinely unknown effect that could invalidate deterministic verification, recovery, or the description-only blast radius remains a blocker.

Deferred / outside first-WRITE semantic scope

The following are retained as useful future evidence but do not block the description-only first WRITE unless implementation review shows that they affect a mandatory invariant above:

  • address/member or detail mutation;
  • alias create, delete, recreate, duplicate-name creation, or byte-identical recreation;
  • forced numeric-locator manipulation outside normal description execution;
  • full Stage 3F;
  • D6 as a standalone empirical race;
  • complete D/E/G permutation matrices and repeated cosmetic fault variants;
  • generic ScenarioId orchestration or complete Stage3 execution-port work;
  • exhaustive broader-resource mutation evidence.

Context

ADR-025 B1 introduced the inert PreparedExecutionIntentV1; B2 introduced signed plan/step-to-execution-digest associations. B3 then stopped because no concrete capability adapter/preparer supplied authoritative values for the B1 execution/recovery tuple.

The repository already contains two narrower owner decisions. ADR-016 permits disposable-lab research on firewall-alias description-only mutation, and ADR-020 names that operation as the Milestone 0 first candidate. Neither decision proves appliance behavior or authorizes implementation. This ADR turns the named candidate into one reviewable semantic specification and defines the evidence required before separately authorized W1 implementation.

The API is the community-maintained pfSense REST API package, not a Netgate-supported interface. Source review is useful design evidence, but the exact disposable appliance's generated OpenAPI document and observed behavior remain authoritative for acceptance.

LAB-T1 prerequisite status

The read-only LAB-T1 harness uses a separately owner-approved, lab-only manual attestation gate for dependency surfaces that current APIs cannot enumerate completely. It still automatically checks available firewall-rule and NAT references, and positive, failed, malformed, or incomplete automatic evidence cannot be overridden by an attestation. Attestations bind the exact sanitized lab identity and normalized synthetic alias for at most ten minutes.

This does not establish global absence of alias dependencies and is explicitly unacceptable as a production authorization, dependency, or target-identity mechanism. Completing LAB-T1 does not authorize or begin this ADR's mutation evidence matrix; a successful sanitized read-only preflight must return for owner review first.

B3 blocker

Current SecurityPosturePlan steps are policy/provisioning actions. They do not carry a typed alias target or desired description. WriteEndpoints is empty, and no real adapter exists. Consequently B3 cannot derive capability, endpoint, method, adapter version, resource target, precondition, normalized mutation, snapshot, or rollback version without a separately approved semantic unit.

This ADR specifies that unit. It does not remove the empirical lab gate and it does not solve the typed planner-input prerequisite described below.

Candidate selection criteria

The first semantic unit should minimize blast radius while exercising all important prepared-intent properties: stable resource selection, complete precondition, one closed mutation projection, authoritative read-back, exact rollback, and ambiguous-outcome handling. It must avoid management addressing, routing, authentication, key material, broad service configuration, bulk replacement, create/delete, and apply/reload operations.

Evaluation priorities, in order, are reversibility, blast radius, deterministic effect, exact identity, unique READ, complete fingerprint, closed typing, authoritative post-condition, exact rollback, API stability, least privilege, low appliance-wide coupling, lockout avoidance, absence of broad disruption, and disposable-lab testability.

Candidate comparison

The 240-endpoint risk inventory found only two credible early candidates. A third representative object-level candidate is shown to make the rejection boundary explicit; it is not considered credible enough to enter the final ranking.

Rank Semantic unit READ / WRITE Identity Mutation and rollback Risk / decision
1 firewall alias: replace descr only GET /api/v2/firewall/aliases; PATCH /api/v2/firewall/alias exact normalized unique alias name; numeric id is a fresh transport locator and protected lifecycle-continuity guard one bounded string; restore old string after full conflict check High because aliases influence policy, but narrowest evidenced candidate; selected, lab-gated*
2 system tunable: replace descr only existing tunables READ; PATCH /api/v2/system/tunable exact tunable name; numeric ID locator one string; restore old string High* and system-wide; description/value coupling and runtime effects are less favorable; fallback only
3 DNS resolver host-override metadata GET /api/v2/services/dns_resolver/host_overrides; singular PATCH endpoint in upstream inventory compound host/domain key is mutable and duplicate/normalization behavior is unproven likely service-config write and resolver reload; exact rollback depends on full object High/service-wide; rejected for first adapter because identity, reload, and rollback are materially more complex

Interfaces, DHCP mappings, certificates/ACME, firewall rules/NAT, routing, users/authentication, and bulk endpoints are rejected categorically for the first adapter due to lockout, credential, ordering, service, or replacement risk. No third candidate in the repository currently matches the top two's combination of READ coverage, non-numeric natural key, and narrow reversible field projection.

Selected candidate

Recommended first capability semantic unit: replace only the description of one existing ordinary firewall alias identified by its exact normalized alias name, using PATCH /api/v2/firewall/alias, while preserving name, type, members, member details, placement, and all apply controls.

This is an update-only operation. It cannot create, delete, rename, reorder, retag, change members, change member details, submit bulk data, request apply, or invoke /api/v2/firewall/apply.

Capability semantics

  • Capability: existing inactive Capability.ALIAS_WRITE.
  • Conceptual operation: set_firewall_alias_description_v1.
  • Risk class: High* at endpoint level; CONFIGURATION_CHANGE authorization. The narrow projection lowers blast radius but does not relabel the broad upstream endpoint as low-risk.
  • Endpoint symbol proposed for later implementation: FIREWALL_ALIAS_DESCRIPTION.
  • HTTP method: PATCH.
  • Endpoint: singular/item update endpoint /api/v2/firewall/alias.
  • Collection replacement (PUT /firewall/aliases) is forbidden.
  • Partial-PATCH semantics are source-documented but must be proven against the pinned lab package. Whole-object replacement is unacceptable.
  • apply, async, placement, append, and remove controls are forbidden as caller inputs. The request must explicitly keep apply=false; W1 must stop if the lab schema cannot express that without an implicit reload.

The upstream model at the reviewed commit declares alias name unique and non-editable, accepts an empty description, and supports singular PATCH. Its apply hook can reload the firewall filter. Whether description-only PATCH without apply performs any runtime reload or unrelated normalization is an empirical acceptance gate, not assumed here.

Resource natural identity

The natural identity is:

{"alias_name": "<NFC-normalized exact alias name>"}

Alias name is chosen because the reviewed upstream model declares it unique and non-editable. Matching is exact and case-sensitive after NFC normalization; no prefix, wildcard, case-folding, or numeric-ID identity is allowed. The lab must confirm the API and pfSense configuration enforce those properties.

The numeric id returned by READ is a transport locator, not semantic identity. The current pfSense API exposes no independent stable generation/incarnation marker. Therefore the ID observed at lifecycle protection time is also sealed as an incarnation-continuity guard. Every execute and rollback read resolves the exact natural name to exactly one row, requires the current ID to equal that guard, and only then projects the freshly read ID into request construction.

Any ID change during the protected lifecycle fails closed. This does not prove that the alias was deleted and recreated; it means non-recreation cannot be proved. A benign renumbering is consequently an accepted false-positive that requires re-prepare/operator retry. Automatic continuation across a locator change is forbidden. The ID remains outside resource_target and semantic identity, cannot be caller supplied, and is never cached by the stateless adapter.

Rename is outside this capability. Deletion/recreation under the same name is treated as a replacement unless the complete precondition remains identical; the later execution gate must fail on any mismatch. Backup/restore and reboot stability of names and ID reassignment require lab characterization. Appliance identity remains separate and unresolved.

System/built-in aliases, URL-table aliases, and other alias types not accepted by the pinned ordinary-alias model must fail closed. The initial lab resource must be a synthetic, ordinary host, network, or port alias with no role in management access or production-equivalent policy.

Authoritative READ

The authoritative read operation is the existing verified GET /api/v2/firewall/aliases, requested with identifying metadata enabled and pagination sufficient to inspect the complete ordinary-alias collection.

The future adapter must:

  1. normalize and validate the requested natural name;
  2. enumerate all pages, not rely on a default/partial limit;
  3. filter by exact normalized name;
  4. reject zero or more than one match;
  5. reject malformed or incomplete objects;
  6. return id, name, type, descr, ordered address, and ordered detail from the same authoritative response.

The current public READ model privacy-defaults address and detail to redacted. A future internal preparer read may request those identifying fields only for contract preparation and must not expose them through MCP output, logs, errors, or reports. This narrow internal use does not change the public privacy default.

If the lab's singular GET /api/v2/firewall/alias?id=... is proven to return the same complete object, it may be used only after natural-name resolution; the plural exact-name scan remains the authority for uniqueness.

Fingerprint and precondition

The canonical target precondition is the full semantic ordinary-alias object, excluding only the transient numeric locator:

{
  "name": "<exact normalized name>",
  "type": "host|network|port",
  "descr": "<current normalized description>",
  "address": ["<ordered member>", "..."],
  "detail": ["<ordered member detail>", "..."]
}

Every field is load-bearing:

  • name proves the natural target;
  • type prevents mutation after semantic retagging;
  • descr provides the expected pre-state and detects concurrent description edits;
  • address protects policy-affecting members;
  • detail protects member-to-detail correspondence and detects server-side regeneration or concurrent edits.

List order is preserved because address/detail positions correspond. NFC string normalization follows B1 canonical rules; no sorting or case folding is added. Numeric ID, API envelope, pagination data, response timestamps, config-history revision numbers, runtime alias-table counters, and apply-status timing are excluded because they are locators/volatile metadata rather than object semantics. Config-history evidence may be retained as lab/audit evidence but is not the resource fingerprint or an automatic global rollback source.

Any missing/redacted field, inconsistent address/detail cardinality, duplicate name, unsupported type, or noncanonical value fails preparation.

Typed mutation projection

The future typed planning input is conceptually:

AliasDescriptionChangeV1
  alias_name: constrained alias-name string
  description: NFC-normalized string, 0..1024 evidenced units

The evidenced boundary accepts 1024 and rejects 1025. Embedded NUL, rejected control characters (including U+0001), malformed/non-string values, and invalid Unicode scalar values are refused. The established whitespace, tab, newline, Unicode and NFD-to-NFC behavior is preserved by the typed boundary rather than reinterpreted during implementation.

Unknown fields are forbidden. Empty description is an explicit value, not missing/null. A requested description equal to the authoritative current value is a no-op and must not create an executable prepared intent. Arbitrary JSON, raw body, numeric ID, type, addresses, details, apply flags, placement, or endpoint/method cannot be supplied.

The normalized B1 mutation intent is:

{
  "operation": "set_firewall_alias_description_v1",
  "raw_target_hint": {"alias_name": "<normalized name>"},
  "new_description": "<normalized description>"
}

The operation discriminator prevents reinterpretation. There are no omitted optional keys and no alternate equivalent input form.

Request mapping

The later adapter must build exactly one closed request from the resolved live locator and normalized intent:

  • method: PATCH;
  • path: /api/v2/firewall/alias;
  • body: exact lab-verified selector plus descr and explicit apply=false;
  • no query parameters;
  • no placement, append, remove, async, dry_run, name, type, address, detail, or additional field;
  • content type: the repository's existing canonical JSON request mechanism.

The expected source-backed selector is numeric id; the provisional body is therefore {id: resolved_id, descr: new_description, apply: false}. This is a lab acceptance hypothesis, not implementation authority. The exact pinned OpenAPI document must confirm field location, types, and whether explicit apply=false is accepted. If ID is a query parameter, if unrelated fields are required, if omitted fields reset, or if apply/reload cannot be suppressed, this candidate fails and W3 must not enable the capability.

No unrelated field is round-tripped into the PATCH. The reason for selecting PATCH is precisely to avoid whole-object replacement. If the lab disproves partial preservation, the operation is rejected rather than changed into a full-object request.

Post-condition

HTTP success is not sufficient. After a successful or ambiguous response, the adapter re-enumerates aliases through the authoritative READ and resolves the exact natural name again.

Verified success requires exactly one match, descr == new_description, and name, type, ordered address, and ordered detail equal the pre-state. The numeric ID is not semantic identity, but the API exposes no independent incarnation marker. The ID captured when the lifecycle is protected is therefore a continuity guard: every authoritative read before or after a send must resolve the same ID. Any change means continuity is unproven and fails closed; the operation never infers safe renumbering or recreation.

Zero/multiple matches, malformed data, a different description, or any forbidden-field difference fails semantic verification. Timeout or lost response never causes automatic replay; authoritative read-back determines verified success versus reconciliation.

First-WRITE acceptance must establish the side-effect boundary in the matrix above. Any apply/reload or policy effect that broadens the operational contract or invalidates deterministic verification/recovery is disqualifying; absence is never inferred solely from HTTP success.

Rollback snapshot

The rollback snapshot is the same complete canonical semantic object used for the precondition: name, type, old descr, ordered address, and ordered detail. It is a full semantic snapshot, not a serialized API envelope and not a global pfSense configuration revision. Numeric ID is intentionally excluded from this semantic snapshot, but is separately integrity-bound as the lifecycle-continuity guard and freshly resolved at every send boundary.

The rollback mutation itself changes only descr back to the captured value. The full snapshot exists to prevent rollback from overwriting unrelated edits, not to send those fields back to the API.

Delete/recreate, rename, duplicate name, type/member/detail change, or missing target stops automatic rollback and enters the existing failure/reconciliation path. Global config-history restore and resource recreation are forbidden.

Rollback policy and version

Proposed rollback policy identifier:

firewall-alias-description-rollback/v1

Rollback is allowed only when the current exact-name read returns one object whose name/type/address/detail equal the snapshot, whose description equals the successfully written new description, and whose target reservation/state permits rollback. It resolves a fresh numeric ID, requires exact equality with the protected lifecycle guard, PATCHes only the original description with apply=false, and performs the full authoritative read-back plus continuity check again.

Rollback succeeds only when all semantic fields equal the snapshot. Any concurrent change, ambiguous send, missing/multiple resource, reload effect, or verification failure stops automatic compensation. There is no generic retry, create-on-missing behavior, or global config restore.

The full pre-forward fingerprint remains unchanged and includes the original description. On successful authoritative forward read-back, the executor must derive and integrity-seal a distinct expected post-forward fingerprint covering the same complete tuple, including the new description, atomically with the VERIFIED transition. The immediate pre-rollback READ must match that sealed post-forward fingerprint exactly. Rollback then restores and verifies the original snapshot/fingerprint. This corrects the pre-evidence finding that a comparison against the original fingerprint would reject every successful description mutation. It does not weaken any fingerprint field.

Architecture-remediation implementation status

The inert Tier 1 contract/store/executor path now implements the distinct, durable expected-post-forward fingerprint boundary and focused synthetic regression coverage. Confirmed-applied reconciliation evidence also signs this fingerprint before it can produce a rollback-eligible VERIFIED contract. This is architecture validation only: no alias adapter, public endpoint, capability activation, production construction, or lab PATCH is introduced. ADR-026 is Accepted; the matrix above records which first-WRITE evidence remains.

First-WRITE acceptance must prove exact description rollback and preservation of members, details, type, and ordering. Runtime/unrelated effects are governed by the explicit side-effect and concurrency boundaries above rather than by an exhaustive broader-mutation campaign.

Adapter and preparer version

Proposed adapter/preparer identifier:

firewall-alias-description/v1

A version bump is required for any change to endpoint or method, selector location/type, request fields, description normalization/bounds, resource identity, fingerprint fields or ordering, mutation-intent shape, read-back or post-condition semantics, apply behavior, snapshot, rollback policy, or supported upstream pfSense/API package compatibility range.

The pinned upstream package commit/version and generated OpenAPI fingerprint are compatibility evidence, not replacements for adapter_version. New software that cannot reproduce the exact B1 intent/digest fails closed.

Complete B1 field mapping

B1 field Exact source and representation Live/caller influence Remaining prerequisite
capability immutable registry entry Capability.ALIAS_WRITE no caller choice W1 adds the inert binding; W3 activates it
endpoint_symbol immutable registry value FIREWALL_ALIAS_DESCRIPTION no caller choice exact symbol/allow-list remains unimplemented
http_method immutable PATCH no caller choice pinned lab OpenAPI confirms
adapter_version immutable firewall-alias-description/v1 no caller choice accepted; W1 pins evidenced compatibility
resource_target {"alias_name": normalized exact typed input} caller requests name; signed V2 binding and live READ prove exact unique match W1 typed request/binding
target_precondition full live semantic object: name/type/descr/address/detail authoritative live READ; caller cannot replace established evidence; W1 production type
normalized_mutation_intent fixed operation + bound target + normalized description typed input only; no raw JSON W1 exact intent binding
rollback_snapshot same full live semantic object authoritative live READ established evidence; W1 production type
rollback_plan_version immutable firewall-alias-description-rollback/v1 no caller choice accepted

All fields are deterministic given the exact typed plan input, one complete authoritative read, and fixed registry/version. Live state is intentionally load-bearing; later B5 must re-read/reprepare before consumption.

Planner relationship

Current PlanStep prose does not select this semantic unit and must never be parsed as authority. PlanDigest/PlanStep v1 remains unchanged. The typed AliasDescriptionChangeV1(alias_name, description) is authoritatively prepared, and PlanAuthorizationV2 signs its exact execution-intent digest beside the exact authorized step ID and plan digest. This is the accepted cryptographic association; W1 must not create a second planning schema or infer mutation facts from action/description text.

This is not a second policy engine: the planner decides that the typed request belongs in the reviewed plan; the adapter only translates the closed request and authoritative state into the fixed execution tuple.

B2 relationship

The future B3 preparer remains authorization-independent. It selects the approved registry entry from the typed semantic-unit discriminator, performs the authoritative read, builds PreparedExecutionIntentV1, and invokes only compute_execution_intent_digest().

The signing workflow then places that recomputed digest beside the exact step ID in PlanAuthorizationV2. B3 does not verify signatures. Future B5 must reprepare from authoritative inputs and compare the exact B1 digest with B2's signed pair before consumption.

Threat analysis

Threat Classification and treatment
wrong or changed numeric ID prevented: every fresh exact-name READ must match the integrity-bound lifecycle guard; mismatch fails closed and is never caller-overridable
duplicate/mutable name lab must prove uniqueness/non-editability; zero/multiple fail closed; otherwise unacceptable
stale precondition / change between read and send detected by executor re-read/fingerprint; fail closed before send
omitted field resets or hidden default lab acceptance gate; any unrelated change rejects candidate
body parameter injection prevented by closed typed request and extra=forbid
endpoint/method substitution prevented by immutable registry and B1 digest/contract binding
delete/recreate changed locator fails closed even when name and full fingerprint are byte-identical; the API cannot prove incarnation continuity
rollback overwrites legitimate edit prevented by full pre-rollback conflict check; conflict enters reconciliation
partial success / timeout after server success detected by read-back; no automatic replay; ambiguous state enters reconciliation
duplicate request/retry prevented by sealed one-send semantics; direct out-of-band API use remains outside coordinator
filter reload/service effect lab acceptance gate; unexpected reload is unacceptable for first adapter
post-condition ambiguity detected/fail-closed by exact full read-back
rollback failure existing rollback-failed/reconciliation state; no repeated blind rollback
privilege mismatch least-privilege lab identity must prove exact operation; denial fails before effect
direct API use outside coordinator deferred operational threat; production construction/isolation remains separate
malformed canonical values prevented by typed model and B1 canonical validation
planner/adapter version mismatch detected by semantic discriminator, adapter version, and digest mismatch
appliance substitution prevented by ADR-025's accepted configured-target/TLS plus stable installation-identifier binding; unavailable or changed identity fails closed

No threat marked as an empirical acceptance gate may be treated as prevented until the lab evidence exists.

Disposable-lab evidence plan

No production firewall, identity, address, certificate, credential, or packet capture may be used. The existing disposable-lab containment/reset plan is authoritative. The package version must be pinned, its generated OpenAPI retained by non-sensitive hash/version, and the synthetic alias must not be referenced by management or production-equivalent rules.

Baseline

  1. Create one synthetic ordinary alias and a second control alias.
  2. Record full plural and singular READ responses without retaining secrets.
  3. prove exact name uniqueness and non-editability; test duplicate creation and case/Unicode variants;
  4. reboot and restore to characterize name stability and numeric ID behavior;
  5. capture canonical semantic snapshot/fingerprint and config/runtime hashes that exclude credentials and identifying data.

Mutation

  1. Compare the generated OpenAPI schema with the pinned upstream source.
  2. Exercise dry-run if supported and prove no config/apply effect.
  3. PATCH only ID, description, and explicit apply=false as confirmed by the schema; record method/path/status/timing, never credentials.
  4. Re-read and prove description equality plus byte-semantic equality of every forbidden field and the control alias.
  5. Inspect config history, dirty/apply status, filter reload evidence, runtime alias table, and service/process state for side effects.

Rollback

  1. Resolve the target again by exact name and verify the forward-state precondition.
  2. PATCH only the old description with apply disabled.
  3. Re-read and prove exact canonical snapshot restoration and unchanged control resource/runtime state.
  4. Repeat after process and appliance restarts where safe.

Concurrency and ambiguity

  • Change description externally between prepare/read and send; expect refusal.
  • Change name/type/member/detail externally; expect refusal.
  • Change a separate alias; measure whether execution can safely proceed without confusing global config revisions.
  • Delete/recreate and reorder aliases; prove natural-name resolution and that any lifecycle ID change refuses conservatively because incarnation continuity cannot otherwise be proven.
  • Drop connection during upload, drop response after commit, and timeout during read-back; prove no automatic second PATCH and deterministic reconciliation.
  • Conflict after forward verification but before rollback; prove rollback stops.

API behavior and repeatability

Test unknown/extra fields, omitted fields, null/empty description, over-limit and invalid Unicode/control values, wrong ID, stale ID, duplicate names, malformed bodies, wrong method, insufficient privilege, authentication failure, dry-run, apply controls, async behavior, response schemas, config locking, config-history failure, restart, and external/manual edits.

The completed 25 clean cycles and completed representative repetitions are not repeated merely to increase counts. Additional repetitions apply only to a remaining mandatory case where they add independent evidence. Any nondeterministic semantic result or operationally material unexplained side effect rejects the candidate.

Evidence acceptance criteria

The first-WRITE acceptance matrix at the top of this ADR is authoritative. Every MUST COMPLETE row must be established before W3 owner enablement; DEFERRED evidence is not PASS and is not a description-only prerequisite. All B1 fields must remain derivable without caller-trusted parallel facts, and logs/results must contain no credentials, stable appliance identifiers, or unnecessary identifying data.

Failure of identity uniqueness, omitted-field preservation, apply suppression, exact rollback, deterministic read-back, least privilege, retry suppression, or operationally material side-effect containment rejects the candidate rather than weakening the design.

Rejected candidates

  • System-tunable description-only PATCH: the only credible fallback, but a tunable is system/kernel configuration and value/description coupling or runtime effects could be more severe. Consider only if alias evidence fails for an alias-specific reason that does not also invalidate tunables.
  • DNS resolver host-override metadata: compound mutable identity, service-reload coupling, full-object preservation, and rollback ambiguity.
  • DHCP static-mapping metadata: DHCP service/config coupling and compound MAC/address identity make rollback and side effects higher risk.
  • Interface description: interface endpoint is critical and can affect management/network configuration or apply semantics; unacceptable first blast radius.
  • Certificate/ACME metadata: key/certificate lifecycle and external ACME side effects; rollback and privilege impact are not local metadata concerns.
  • Alias members or create/delete: directly changes firewall policy inputs, expands mutation shape, and complicates rollback/references.
  • Bulk endpoints: omission means replacement/deletion; unacceptable.

Resolved owner decisions

The owner accepts:

  1. Capability.ALIAS_WRITE for description only;
  2. singular PATCH /api/v2/firewall/alias and endpoint symbol FIREWALL_ALIAS_DESCRIPTION;
  3. normalized exact alias name as natural identity and numeric ID only as a fresh protected lifecycle locator;
  4. the complete name/type/description/ordered-address/ordered-detail fingerprint;
  5. model-facing AliasDescriptionChangeV1 inputs alias_name and description, with existing evidence-derived normalization and bounds: NFC intent, maximum 1024 accepted units, 1025 rejected, malformed/non-string and U+0001 control input rejected, and empirically accepted whitespace/ Unicode behavior retained exactly rather than re-guessed;
  6. the full semantic rollback snapshot;
  7. rollback policy firewall-alias-description-rollback/v1;
  8. adapter/preparer version firewall-alias-description/v1, pinned to the evidenced upstream compatibility range;
  9. the semantic-scope-specific evidence matrix above;
  10. PlanDigest/PlanStep v1 unchanged, with the exact step-to-intent association signed by PlanAuthorizationV2 as accepted in ADR-025.

W1 implementation boundary

A separately authorized W1 may add the production capability-specific typed request, adapter and authoritative preparer, then compose the accepted ADR-025 PlanAuthorizationV2-to-provenance-bound-RecoveryContract chain through one MutationExecutor handoff. W1 must remain production-unreachable from MCP and must not populate WriteEndpoints, activate ALIAS_WRITE, register a tool, or perform live mutation. W2 owns fixed production construction; W3 alone owns the endpoint/capability/tool surface and selected live acceptance.

Live first-WRITE evidence (2026-08-16)

The first real pfSense mutation in this project's history occurred against the disposable LAB appliance (LAB_ALIAS_TEST) under full owner authorization, ceremony-by-ceremony, per-artifact independent verification (full detail: reports-ai/reviews/SLICE6_PREVIEW_DESCRIPTION_TRUNCATION_INVESTIGATION_2026-08-16.md, external/git-ignored). Evidence reconstructed independently from authoritative persisted state during this consolidation pass, not copied from that report.

Directly observed, not inferred:

  • Exactly one mutating pfSense request occurred: PATCH /api/v2/firewall/alias, apply=false — confirmed both from the executor's own log line at execution time and from AliasDescriptionPatchV1.apply being hardcoded False at both call sites in tier1/alias_description.py (never a caller-controlled value).
  • Deterministic authoritative read-back: LAB_ALIAS_TEST.descr matched the requested value exactly, both immediately after execution and again independently during this later consolidation pass.
  • RecoveryContract aliasdescr-68313213fc7c47a9b070d5e480d2ad70 reached VERIFIED/state_version=4/is_confirmed=True. store.load()'s internal _verified_audit_rows() (integrity MAC chain, ordering, completeness, terminal-state agreement) succeeded without raising — re-verified independently, not merely re-read — for this contract and for both prior expired/dead contracts.
  • Full audit chain: contract_created(0) → prepared(1) → contract_confirmed(2) → executing(3) → verified(4). No rollback/reconciliation event type appears anywhere in audit_events.
  • Authorization consumed exactly once: authz-d9d846ec9c3b46095c81e97a4ff91402 present exactly once in AuthorizationConsumptionStore; two earlier, unrelated authorizations (from the two now-permanently-expired ceremonies) are also present, each exactly once, confirming one-time consumption held across all three attempts, not merely the successful one.
  • Witness advanced exactly once: 2 → 3, confirmed via a live TpmHostWitnessAnchor.read(). The store's own independently-persisted anchor_state.high_water_mark also reads 3 — no divergence between physical witness and persisted record.
  • No target-reservation or rate-cooldown residue: both tables read 0 rows post-VERIFIED, consistent with VERIFIED not being a member of _RESERVATION_STATES (EXECUTING/RECONCILIATION/ROLLING_BACK/ ROLLBACK_FAILED only) — the reservation held during EXECUTING was correctly released on reaching the terminal state, not merely absent.
  • No mutation occurred during the two earlier, expired ceremony attempts: both dead contracts remain at PREPARED/state_version=1, never reached EXECUTING, and the alias's description before this consolidation pass's own investigation matched exactly what the successful ceremony set it to — no unaccounted-for intermediate value was ever observed.
  • Fail-closed expiry, demonstrated separately from success: two real, live attempts were correctly refused before execution purely on freshness grounds (a ConfirmationEvidence past its own expires_at, and — architecturally, via resume_prepared()'s own checks — two contracts whose bound authorization window had elapsed), each independently confirmed to have produced zero pfSense contact and zero state mutation.

get_firewall_apply_status() finding (2026-08-16, first consolidation pass): the LAB appliance reports applied=False, pending_subsystems=['aliases'] following the real WRITE. This is apply=false's documented pfSense v2 API consequence, not a defect — the config.xml entry is written but the running ruleset is not regenerated. Since descr is pure metadata never compiled into the firewall ruleset, this has no live traffic-behavior effect, but it does mean the appliance carries a standing "pending changes" indicator this architecture's WRITE path does not clear (no explicit apply step exists by design). This directly, positively confirms row 6 (apply=false genuinely suppresses any reload/apply, observed on real hardware, not merely coded).

Row 18 (side-effect evidence) — config-history clause closed, 2026-08-16 second consolidation pass: a new, narrowly-scoped read-only client method, PfSenseClient.get_config_history_revisions() (/api/v2/diagnostics/config_history/revisions, previously DIAGNOSTICS_CONFIG_HISTORY_READ in docs/READ_BACKLOG.md — planned but unimplemented), was added and used to read the LAB appliance's full config-history/backup revision list directly. Result: the most recent revision entry predates the entire Slice 6 ceremony workstream by several days; zero config-history revisions exist for the WRITE date. This is direct, authoritative, live-observed evidence — not inference — that this architecture's apply=false alias-description PATCH does not create any config-revision/backup entry at all (the backup-creation step appears bound to pfSense's own "apply" action, which this path never triggers by design). This closes the one item that kept row 18 at PARTIALLY VERIFIED: there is no unknown config-history/backup proliferation effect. Row 18 is now VERIFIED.

Row 17 (least privilege) — root-caused, now positively contradicted by live evidence, 2026-08-16 second consolidation pass: the original get_users() failure was root-caused, not merely logged for later. Exact cause: PfSenseUser.expires was typed as non-nullable str, but the real LAB appliance's single local user account (admin) legitimately returns expires: null (no expiration configured) — a genuine model/schema defect (the approved test fixture had only ever exercised the empty-string case), not a permissions issue, not API-version drift, not fixture-only drift. Fixed narrowly (expires: str | None), with a regression test and an approved-contract-snapshot update; no other field or code path was affected (confirmed via git diff against the public contract snapshot: only the expires field's schema changed).

With get_users() now working, the evidence is materially worse than "unproven": the LAB appliance has exactly one local user, admin, scope: system, priv: ['user-shell-access'] — full administrative scope. RestApiClient's identity parameter (PFSENSE_IDENTITY, e.g. pfsense_lab1) is confirmed, by direct code inspection, to be used only for local log-line correlation (logger.warning(...) call sites) and is never sent as an HTTP header or otherwise presented to pfSense — authentication is exclusively via the X-API-Key header. Since the appliance has no other local user for that key to be associated with, and the config-history revision descriptions independently corroborate this ("admin@<lab-ip>: Modified Firewall Alias via API" for every ceremony mutation, matching the live evidence above), the API credential used for the entire Slice 6 ceremony workstream, including the first real WRITE, authenticates as pfSense's full administrative account, not a capability-scoped identity. This is not caller discipline standing in for an enforced boundary — it is direct evidence that no least-privilege boundary exists at the appliance-identity level for this LAB at all, since only one (full-privilege) local user is provisioned. Row 17 remains MUST COMPLETE, now with an identified, concrete remediation path: provision a second, capability-scoped pfSense local user/API key (e.g. limited to the alias-write privilege class) and re-run the acceptance evidence against that identity. This requires live pfSense user/credential provisioning, which is explicitly outside any autonomous session's authority — an owner/security-policy action.

ADR-026 acceptance-matrix closure evidence (2026-08-16, third consolidation pass)

Seven of the eight remaining MUST COMPLETE rows (concurrent-change refusal, stale-expected-state refusal, rollback-from-verified-B, no-blind-retry, uncertainty classification, fail-closed reconciliation, authenticated recovery across restart) were investigated for the first time this pass. For each, the question was: does production-bound offline test evidence (the real AliasDescriptionAdapterV1/ AliasDescriptionExecutionCoreV1/build_production_runtime() composition, not a generic synthetic adapter) already exist, and does it genuinely prove the row's stated requirement?

Finding: six of the seven rows already had exactly this evidence — test_alias_description_execution.py::test_production_adapter_drift_refuses_before_executor_send and test_production_adapter_rollback_conflict_refuses_and_post_expiry_recovery_remains_available were added 2026-08-12; test_production_runtime.py::test_restart_reconciles_interrupted_executing_contract and test_reconciliation_requires_pinned_authority_and_wrong_signature_fails_closed were added 2026-08-14 — both dates before this ADR's evidence sections were written, meaning this evidence existed and simply had not yet been cross-referenced against the acceptance matrix. The seventh (authoritative uncertainty classification) had a genuine, narrow gap: the DEFINITELY_APPLIED/DEFINITELY_NOT_APPLIED/AMBIGUOUS classification was proven only against test_executor.py's generic _SyntheticAdapter, never the real alias adapter. Closed with one new test, test_production_adapter_send_timeout_reaches_reconciliation_not_failed_or_resend.

Interpretation flagged, not silently resolved: several of these rows' original wording (e.g. row 8's "production-bound D1-D5-equivalent focused tests and any owner-selected live evidence must prove the exact adapter path") is genuinely ambiguous about whether live evidence is strictly conjunctive-required or merely an owner-selectable option on top of sufficient offline evidence. This pass adopted the latter reading (offline production-bound evidence is sufficient absent an explicit owner request for additional live evidence) and reclassified these seven rows to VERIFIED (offline, production-bound) on that basis — a status distinct from live-observed VERIFIED rows (6, 18), which remain the only rows backed by real hardware/appliance observation. If the owner's intended reading was strictly conjunctive, these seven rows should be read as MUST COMPLETE pending owner-selected live evidence instead; no code or architecture decision depends on which reading is correct, so this is purely a documentation/acceptance-bar judgment call, reversible by editing this table alone.

Updated, 2026-08-16 (same day, later pass, after the scoped-credential restoration ceremony): row 17 is now also VERIFIED — see "Scoped-credential live-WRITE evidence (2026-08-16, restoration ceremony)" below. Every row in the acceptance matrix now reads ESTABLISHED or some flavor of VERIFIED; none reads MUST COMPLETE.

Still not decided by this update, and still the sole remaining gate before WriteEndpoints.FIREWALL_ALIAS_DESCRIPTION.verified could be set True: the interpretation question flagged immediately above. Seven rows carry the VERIFIED (offline, production-bound) status only under the reading that offline, production-bound evidence is conjunctively sufficient absent an explicit owner request for additional live evidence. That reading was proposed by this ADR's own documentation pass, not confirmed by the owner. An autonomous session choosing the more permissive reading itself, purely to unlock verified=True, would be exactly the "reinterpret acceptance criteria to force a yes" this project's standing discipline forbids — so this pass does not flip verified=True and does not resolve the interpretation itself. If the owner confirms the offline-evidence reading, verified=True becomes justified without further evidence-gathering. If the owner instead intends the strictly-conjunctive reading, those seven rows revert to MUST COMPLETE pending owner-selected live evidence, and verified=True remains blocked pending that live evidence. See reports-ai/NEXT_TASKS.md for the exact open decision packet.

Resolved, 2026-08-16 (same day, later pass) — owner confirmed the evidence-interpretation standard and a strict, non-grandfathering row-by-row re-check was performed against it before verified=True was touched. The owner's confirmed standard: production-bound evidence may satisfy a row only when (a) it exercises the real production implementation and security boundary — never a generic/synthetic test double, (b) only the external transport/appliance side is substituted or mocked, (c) the test proves every conjunctive requirement the row's own wording states, not a subset, (d) the behavior is tied directly to the relevant ADR invariant, not an incidental side effect, and (e) no contradictory live evidence exists anywhere in this project's history — explicitly not permission to reinterpret an explicit live-evidence requirement as an offline-test requirement.

Each of the seven rows was re-checked against this standard from scratch, not grandfathered from the prior pass's classification:

  • Re-read each row's exact wording directly in this table (not a paraphrase from a prior pass).
  • For the three tests underpinning the concurrent-change/stale-state refusal, rollback-conflict, and uncertainty-classification rows, the actual test source (tests/tier1/test_alias_description_execution.py) was read directly, confirming each constructs the real AliasDescriptionAdapterV1, the real SqliteRecoveryContractStore, and the real MutationExecutor via _sealed_executor() — only the transport-facing _ReadClient/_WriteClient/_TimeoutWriteClient test doubles stand in for pfSense itself.
  • For the reconciliation and restart-recovery rows, the underlying test (tests/tier1/test_production_runtime.py::test_restart_reconciles_ interrupted_executing_contract) was confirmed to call build_production_runtime() — the actual fixed production composition itself — twice, simulating a real restart; the pinned reconciliation-authority test was confirmed to use the real runtime.resolve_reconciliation() path.
  • All 19 tests underpinning the seven rows were re-run directly (2823 passed / 42 skipped, full suite, zero regressions) as part of this re-check, not merely cited from memory.
  • Checked each row's own wording, and the ADR's own original acceptance standard (see "Response-loss and timeout boundary" and "Side-effect boundary" above: "New live evidence is required only for material behavior introduced by the fixed production composition") for any explicit live-appliance requirement. None of the seven rows' wording demands live-appliance observation specifically — they concern the Tier1 security state machine's own internal behavior (refusal, reconciliation, uncertainty classification, restart recovery), which this ADR's own original standard already treats as legitimately offline-provable through the real production composition; live appliance behavior itself (side effects, config-history, apply suppression) is exclusively rows 6/18's domain, already independently live-verified.
  • Checked for contradictory live evidence from either real WRITE (the first WRITE and the scoped-credential restoration): neither exhibited a rollback, reconciliation, replay, or uncertain outcome — both completed cleanly in one send each — so there is no live observation anywhere in this project's history that contradicts any of the seven rows' offline-proven claims.

All seven rows independently survived the strict re-check. No row was reverted. WriteEndpoints.FIREWALL_ALIAS_DESCRIPTION.verified was therefore set True (commit f70b9f18dd2c5107ef04bf2bdc249b02fddf2a76). This does not change tool registration (governed separately by profile grant + WriteEndpoints entry + constructed runtime, none of which this change touches) or default MCP exposure — confirmed mechanically, not merely argued: make quick's write-capability-inactivity check reports 0 of 3 *_WRITE capabilities are default-reachable, and make validate's public-contract snapshot reports 42 tools, both unchanged from before this commit.

Least-privilege identity design and provisioning procedure (2026-08-16)

Full design (exact pfSense endpoint/privilege mapping, proposed scoped identity, owner-run provisioning procedure, post-provisioning verification matrix) is maintained as a dedicated, git-ignored reports-ai document rather than duplicated into this ADR: reports-ai/reviews/SLICE6_LEAST_PRIVILEGE_PROVISIONING_2026-08-16.md. Summary: the production WRITE path needs exactly four pfSense API capabilities (GET /api/v2/firewall/aliases, GET /api/v2/status/system, conditionally GET /api/v2/system/hasync, and PATCH /api/v2/firewall/alias) — nothing else. get_users()/get_firewall_apply_status()/ get_config_history_revisions() are evidence-gathering diagnostics, not production runtime dependencies, and the scoped identity does not need them. The exact pfSense native privilege identifier strings could not be enumerated via a safe read-only API call (this pfSense REST API build exposes no privilege-catalog endpoint); the provisioning procedure has the owner select privileges live via pfSense's own GUI picker against the documented capability list, then closes the loop by reading back the new user's actual priv list via get_users() for owner verification — never guessing an unverified privilege ID string.

Superseded, 2026-08-16 (same day, later pass): the GUI-picker plan above was not what actually happened. Owner-authorized, source-level discovery of the exact pfSense REST API package privilege identifiers (pfrest/pfSense-pkg-RESTAPI, pinned to the exact installed tag v2.10.0 — matches the live OpenAPI schema's reported version) replaced it: Core/Endpoint.inc::get_method_priv_name() computes a deterministic api-v2-<url-slug>-<method> privilege string per endpoint/method, entirely separate from the legacy page-* GUI privileges this ADR originally assumed would be used. Full derivation, per-privilege source citation, and the live provisioning transcript are in reports-ai/reviews/SLICE6_LEAST_PRIVILEGE_PROVISIONING_2026-08-16.md (external/git-ignored). No GUI privilege-picker step was ultimately needed.

Scoped-credential live-WRITE evidence (2026-08-16, restoration ceremony)

The disposable-LAB LAB_ALIAS_TEST restoration (its description reverted from the Slice 6 temporary marker back to 'Disposable LAB-T1 synthetic test alias') was executed as the second real pfSense mutation in this project's history, entirely through the newly-provisioned scoped identity pfsense_mcp_tier1_lab — the admin credential was never used for this WRITE. Full ceremony detail: reports-ai/reviews/SLICE6_PREVIEW_DESCRIPTION_TRUNCATION_INVESTIGATION_2026-08-16.md (external/git-ignored). Evidence below reconstructed independently from authoritative persisted state, not copied from that report.

Row 17 evaluated against its own exact wording — "least privilege for exact endpoint/capability" — conjunctively, not as a single pass/fail:

  1. A capability-scoped identity exists, distinct from admin. pfsense_mcp_tier1_lab, scope=user (pfSense-assigned, read-only field — never system), independently re-read via the admin account both before and after this ceremony.
  2. It holds only the minimum privilege the exact endpoint/capability needs — nothing broader. priv == exactly api-v2-firewall-aliases-get, api-v2-firewall-alias-patch, api-v2-status-system-get, api-v2-system-hasync-get — the complete set derived in the "Least-privilege identity design" section above, independently re-read (a separate get_users() call, not the provisioning response echoed back) immediately before this ceremony. page-all was never granted, even transiently during the credential's own API-key bootstrap (which required one additional privilege, api-v2-auth-key-post, granted narrowly and revoked again before this ceremony — see the provisioning report for that sub-sequence).
  3. The identity actually performs the exact production capability under that minimum grant — not a synthetic/raw-API stand-in for it. The restoration ran through the complete, unmodified production Tier1 ceremony (build_production_runtime(), real RecoveryContract, real signed PlanAuthorizationV2/ConfirmationEvidence, real confirm_and_handoff()MutationExecutor.execute()), configured to use the scoped credential for every pfSense contact this path makes — not merely a one-off raw-API probe outside the security architecture. The executor's own log line recorded exactly one mutating request, PATCH /api/v2/firewall/alias, and RecoveryContract aliasdescr-4d8c2310ba0343698688166c56983c04 reached VERIFIED/state_version=4/is_confirmed=True, independently re-verified via store.load()'s internal _verified_audit_rows() integrity-MAC-chain check, not merely re-read.
  4. No broader capability is available to the identity, including immediately after performing the WRITE (no drift/escalation as a side effect). Negative checks performed during provisioning (denied: get_users(), get_system_packages(), get_system_certificates(), and a retry of the identity's own API-key-generation endpoint) were not merely a point-in-time snapshot — the identity's priv list was re-read a second time, independently, after this restoration WRITE completed, and found unchanged from the pre-WRITE grant. Performing the real mutation did not — and structurally cannot, since pfSense's own privilege check is a static per-user grant, not something a successful API call can itself widen — expand what the identity can do.

All four conjunctive parts of row 17's stated requirement are now satisfied by live, independently-verified evidence obtained through the real production execution path. Row 17 is VERIFIED.

Distinct, unresolved follow-up (not part of row 17's own closure criteria, noted for completeness): the scoped credential was used for this ceremony via a temporary environment override (PFSENSE_API_KEY_FILE); the persistent tier1-lab.env default still points at the admin key. Row 17 asks whether the capability can be proven under least privilege, which it now has been — it does not itself require the scoped credential to be the permanent runtime default going forward. Whether to switch the default is a separate, low-risk operational decision left open for the owner.

Corroborating evidence for already-accepted/verified rows (none of these change classification — the existing evidence was already sufficient — but the restoration independently reproduces the same result through a second live mutation, under a different, more narrowly-privileged identity than the first WRITE):

  • Exact authoritative A restoration (already ESTABLISHED via 25/25 offline campaign restorations): this ceremony is a genuine, live restoration-to-exact-A through the real production path — LAB_ALIAS_TEST.descr read back exactly 'Disposable LAB-T1 synthetic test alias', byte-for-byte the original value recorded before Slice 6's first WRITE.
  • At-most-one-send (already ESTABLISHED): a second real executor log confirms exactly one mutating request for this contract, no more.
  • Explicit apply/reload suppression (row 6, already VERIFIED): the same apply=false behavior was observed again, this time under the scoped identity — corroborating that this is a property of the endpoint/method itself, not an artifact of the admin account's broader access.
  • Sufficient authoritative side-effect evidence (row 18, already VERIFIED): likewise, a second independent data point that the observed side-effect profile (deterministic read-back, no config-history entry, no unrelated configuration change) holds regardless of which identity performs the WRITE.

Witness/store state: advanced exactly once, 3 → 4; persisted anchor_state.high_water_mark also reads 4 — no divergence. Consumption store holds the new authorization exactly once (authz-63245f075962f5552802571b51a6f21e), four rows total across the whole workstream, none replayed. target_reservations/rate_cooldowns: 0/0 post-VERIFIED, consistent with the same reservation-release behavior already documented for the first WRITE above.

A genuine operational finding surfaced mid-ceremony, root-caused and fixed, not merely worked around: the signer's independent safety check initially refused to sign ("independently-derived security posture is not currently safe to proceed"). Root cause: the signer's local store.json snapshot still carried high_water_mark=2 from before the first WRITE, while the live witness (correctly) now read 3evidence_state=provisioned_mismatch. This was a stale local cache on the signer, not a security anomaly on the production side (whose own live store was already correctly 3); refreshing the signer's snapshot from the current live store resolved it, and signing then succeeded against the correct, freshly-verified evidence. This is a durable operational lesson for any future ceremony after a witness advance: the signer's cached store snapshot needs refreshing whenever the live witness value changes, or the signer's own fail-closed check will (correctly) refuse to sign.

STOP conditions

Stop W1/W2/W3 at the applicable boundary if a MUST COMPLETE matrix row cannot be established, if exact request semantics or B1 sources diverge from the accepted evidence, if caller-selected raw JSON is required, or if the PlanAuthorizationV2/appliance-target binding cannot be carried through the authenticated contract. W3 must not enable WRITE with an unresolved operationally material side effect or candidate uncertainty.

Public MCP remains 42 READ / 0 WRITE. WriteEndpoints remains empty and WRITE capabilities remain 0/3 active.

References