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_CHANGEauthorization. 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, andremovecontrols are forbidden as caller inputs. The request must explicitly keepapply=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:
- normalize and validate the requested natural name;
- enumerate all pages, not rely on a default/partial limit;
- filter by exact normalized
name; - reject zero or more than one match;
- reject malformed or incomplete objects;
- return
id,name,type,descr, orderedaddress, and ordereddetailfrom 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:
nameproves the natural target;typeprevents mutation after semantic retagging;descrprovides the expected pre-state and detects concurrent description edits;addressprotects policy-affecting members;detailprotects 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
descrand explicitapply=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¶
- Create one synthetic ordinary alias and a second control alias.
- Record full plural and singular READ responses without retaining secrets.
- prove exact name uniqueness and non-editability; test duplicate creation and case/Unicode variants;
- reboot and restore to characterize name stability and numeric ID behavior;
- capture canonical semantic snapshot/fingerprint and config/runtime hashes that exclude credentials and identifying data.
Mutation¶
- Compare the generated OpenAPI schema with the pinned upstream source.
- Exercise dry-run if supported and prove no config/apply effect.
- PATCH only ID, description, and explicit
apply=falseas confirmed by the schema; record method/path/status/timing, never credentials. - Re-read and prove description equality plus byte-semantic equality of every forbidden field and the control alias.
- Inspect config history, dirty/apply status, filter reload evidence, runtime alias table, and service/process state for side effects.
Rollback¶
- Resolve the target again by exact name and verify the forward-state precondition.
- PATCH only the old description with apply disabled.
- Re-read and prove exact canonical snapshot restoration and unchanged control resource/runtime state.
- 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:
Capability.ALIAS_WRITEfor description only;- singular
PATCH /api/v2/firewall/aliasand endpoint symbolFIREWALL_ALIAS_DESCRIPTION; - normalized exact alias name as natural identity and numeric ID only as a fresh protected lifecycle locator;
- the complete name/type/description/ordered-address/ordered-detail fingerprint;
- model-facing
AliasDescriptionChangeV1inputsalias_nameanddescription, 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; - the full semantic rollback snapshot;
- rollback policy
firewall-alias-description-rollback/v1; - adapter/preparer version
firewall-alias-description/v1, pinned to the evidenced upstream compatibility range; - the semantic-scope-specific evidence matrix above;
- PlanDigest/PlanStep v1 unchanged, with the exact step-to-intent association
signed by
PlanAuthorizationV2as 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 fromAliasDescriptionPatchV1.applybeing hardcodedFalseat both call sites intier1/alias_description.py(never a caller-controlled value). - Deterministic authoritative read-back:
LAB_ALIAS_TEST.descrmatched the requested value exactly, both immediately after execution and again independently during this later consolidation pass. RecoveryContract aliasdescr-68313213fc7c47a9b070d5e480d2ad70reachedVERIFIED/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). Norollback/reconciliationevent type appears anywhere inaudit_events. - Authorization consumed exactly once:
authz-d9d846ec9c3b46095c81e97a4ff91402present exactly once inAuthorizationConsumptionStore; 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 liveTpmHostWitnessAnchor.read(). The store's own independently-persistedanchor_state.high_water_markalso reads3— no divergence between physical witness and persisted record. - No target-reservation or rate-cooldown residue: both tables read
0rows post-VERIFIED, consistent withVERIFIEDnot being a member of_RESERVATION_STATES(EXECUTING/RECONCILIATION/ROLLING_BACK/ROLLBACK_FAILEDonly) — the reservation held duringEXECUTINGwas 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 reachedEXECUTING, 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
ConfirmationEvidencepast its ownexpires_at, and — architecturally, viaresume_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 realAliasDescriptionAdapterV1, the realSqliteRecoveryContractStore, and the realMutationExecutorvia_sealed_executor()— only the transport-facing_ReadClient/_WriteClient/_TimeoutWriteClienttest 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 callbuild_production_runtime()— the actual fixed production composition itself — twice, simulating a real restart; the pinned reconciliation-authority test was confirmed to use the realruntime.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:
- A capability-scoped identity exists, distinct from admin.
pfsense_mcp_tier1_lab,scope=user(pfSense-assigned, read-only field — neversystem), independently re-read via the admin account both before and after this ceremony. - It holds only the minimum privilege the exact endpoint/capability
needs — nothing broader.
priv== exactlyapi-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 separateget_users()call, not the provisioning response echoed back) immediately before this ceremony.page-allwas 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). - 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(), realRecoveryContract, real signedPlanAuthorizationV2/ConfirmationEvidence, realconfirm_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, andRecoveryContract aliasdescr-4d8c2310ba0343698688166c56983c04reachedVERIFIED/state_version=4/is_confirmed=True, independently re-verified viastore.load()'s internal_verified_audit_rows()integrity-MAC-chain check, not merely re-read. - 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'sprivlist 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.descrread 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=falsebehavior 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 3 —
evidence_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¶
- ADR-016: Alias-candidate disposable-lab authorization
- ADR-020: First WRITE capability candidate
- ADR-025: Authorization-to-RecoveryContract binding
- Writable endpoint risk matrix
- Tier 1 activation decisions
- Disposable lab execution model
- Capability adapter contract
- pfrest endpoint-type documentation
- pfrest common control parameters
- Netgate alias documentation