I read this as a read-only adversarial V14 r2 release audit - I’ll trace source, preseal, candidate, and rehearsal artifacts end to end, test each named failure mode locally, and return one evidence-backed PASS or FAIL without changing or authorizing anything.


I read this as a read-only adversarial V14 r2 release audit - I traced the sealed execution path and tested every requested failure class without authorization or mutation.

# Verdict: FAIL

V14 r2 must not be authorized. It currently fails closed before execution, and the frozen design also permits target substitution and unsealed source imports.

## Decisive evidence

### 1. Frozen controller closure no longer matches the specified source

`controller-source-manifest.json` contains 311 entries. A final SHA-256 comparison found five mismatches:

- `scripts/nutricoach_v150_controller_worker.py`
- `scripts/nutricoach_v150_detached_bootstrap.py`
- `scripts/nutricoach_v150_sealed_controller.py`
- `scripts/verify_nutricoach_v150_preseal_v14.py`
- `tests/test_nutricoach_v150_v14_safety.py`

For example:

- Expected bootstrap: `1d4b5678...e70f85`
- Current bootstrap: `c1ef8051...0d8c`
- Expected controller: `c511f998...86990`
- Current controller: `0acccdc3...9cf0`

The bootstrap calls `verify_closure(...)` before importing the worker, so r2 now necessarily denies with `closure_drift`. The source also changed during the audit to reference an r3 preseal, demonstrating why a mutable worktree cannot be the execution closure.

### 2. Source import escape exists in the frozen r2 candidate

The bootstrap verifies selected files under the mutable source worktree and then places that whole root on `PYTHONPATH`:

- `nutricoach_v150_detached_bootstrap.py:146-155`

The sealed candidate imports these local modules, but none are present in the controller manifest:

- `scripts/execute_nutricoach_v150_live_upgrade.py`
- `scripts/nutricoach_v150_live_upgrade_boundary.py`
- `scripts/nutricoach_v150_live_upgrade_common.py`
- `scripts/nutricoach_v150_live_upgrade_state.py`
- `scripts/nutricoach_v150_sealed_authority.py`
- `scripts/nutricoach_v150_phase_journal.py`
- `scripts/nutricoach_v150_sealed_target.py`

These include package validation, rollback snapshots, the global authorization ledger, and the real systemd adapter. Therefore critical transaction behavior can be imported from source bytes not covered by `controller-source-manifest.json`.

### 3. Target/package substitution is not prevented

The authorization digest covers the package payload, but these operational target fields exist only in `sealed-target.json`:

- `profile_root`
- `current_runtime`
- `successor_runtime`
- `unit`
- `dropin`
- `execution_root`
- `global_approval_ledger`
- `protected_inventory`
- `permission_package`
- `controller_target_binding`
- `authority_id`

The bootstrap accepts caller-selected `--preseal`. Although `package-manifest.json` hashes `sealed-target.json`, that manifest is self-excluded and is not covered by the authorized package digest. It can therefore be recomputed around a substituted target while retaining the same approval phrase and permission package.

`execute_verified_target()` checks the phrase and controller derivation, then constructs `ConcreteLiveHost` directly from the substituted target. It does not compare those paths against package payload values.

### 4. Candidate and live authority paths are incompatible

Positive evidence: all 321 candidate inventory rows currently exist and match their recorded hashes; candidate digest `f4198225...58d8f` agrees across candidate, preseal, and rehearsal.

However:

- The live package inspector expects `awaiting-authorization.json`, `payload.evidence`, `payload.target`, and `payload.snapshots`.
- The V14 r2 preflight contains only `package.json` and `snapshot-before.json`.
- A read-only call to `inspect_package()` returned:

```text
UpgradeDenied invalid_json:awaiting-authorization.json
```

The public bubblewrap command also omits the bootstrap's required `--preseal` argument. Static inspection returned `has_preseal False`.

The rehearsal does not exercise this path. It calls `execute_disposable(APPROVAL_PHRASE, ...)`, where the controller phrase is based on `DISPOSABLE_V3`, uses `DisposableService`, and only copies the real package digest into the final report afterward.

### 5. Hard-coded stale identities remain in the r2 candidate

The frozen candidate contains:

- Candidate digest `066a794d...` in `nutricoach_v150_live_models.py`, not `f4198225...`
- Old package digest `a362d994...`
- Old `task-1-candidate` wheel paths
- V13 preseal constants in the bootstrap and controller
- `DISPOSABLE_V3` as the controller's default package identity

Some live values are overridden from `sealed-target.json`, but the stale defaults are still used by disposable/rehearsal paths and make those paths non-representative.

### 6. Canonical rollback scope is not authority-bound

Current live shape is as recorded:

- Enabled: `pilot_20260820_01`
- Disabled: `task26_claim_20260818145508_1b96b23d`
- Enabled wizard event and lock files are absent.
- Enabled `canonical-sequence.jsonl` exists, mode `0600`, one link, empty.

The implementation mechanically tracks dynamically missing files in `_created_files` and removes them during rollback. However:

- `authority_created_paths_rollback` is never consumed or enforced by `ConcreteLiveHost`.
- Missing canonical files are recomputed from the runtime registry.
- `protected_paths()` silently excludes a sealed stable path when it no longer exists.
- Preflight capture hashes current runtime bytes rather than comparing them with hashes in `snapshot-before.json`.

Consequently registry drift or disappearance of another canonical file can expand the creation/removal set beyond the authorized rollback list.

### 7. Disabled-draft authorization is not stable

At the captured state, enabled-only filtering is correct: missing-file creation, authority registration, configuration, and startup smoke all skip disabled customers.

But execution does not verify the live registry against sealed hash `bd61ed95...660b29` or the sealed two-customer projection. If the disabled row becomes enabled before execution, the migration authorizes every currently enabled customer, up to five, and can create their canonical files. The presealed disabled-draft restriction therefore is not enforced at execution time.

### 8. Service and post-fence checks are insufficient

Positive evidence: `ConcreteLiveHost.stop()` re-observes service state and rejects a service that remains running.

Failures:

- `SystemdService`, from the unsealed `nutricoach_v150_sealed_target.py`, hard-codes `hermes-gateway-dualcoachtest.service` instead of binding the service identity from the sealed target.
- `switch_postimages()` performs a blind byte replacement and does not require the predecessor runtime to have occurred.
- Post-fence accepts any manager `ExecStart` containing the successor path as a substring.
- It does not require the exact argv/profile contract.
- It does not prove `MainPID` or `ExecMainStartTimestampMonotonic` changed across restart.
- It does not prove the running process loaded the sealed candidate credentials.
- Weekly authority/configuration is not revalidated after service start.

## Evidence that did pass

- All eight preseal package-manifest entries match.
- No unexpected files exist in the r2 preseal.
- Candidate inventories and wheel hashes match.
- Candidate, package, and rehearsal digest fields agree.
- The supplied rehearsal reports 13 passing cases.
- Authorization ledger, execution root, and successor runtime remain absent; no authority was consumed.

These integrity checks do not overcome the execution-path defects above.

## Architecture options

### Option A - One fully bound descriptor and frozen source closure

Put every target path, exact service name/argv, candidate and wheel hashes, registry/inventory hashes, canonical creation list, and complete transitive controller closure into the canonical payload covered by the approval digest. Execute controller files from a read-only preseal copy, not the worktree. Use one package schema from verifier through executor.

- Lowest migration cost
- Keeps current controller structure and tests
- Requires careful complete closure generation
- Main failure mode becomes ordinary digest mismatch

### Option B - Package the controller as a third immutable wheel or zipapp

Build the controller, ledger, rollback, target model, and service adapter into one hash-bound executable artifact alongside the two product wheels.

- Strongest protection against source-import escape
- Cleaner reproducibility and import behavior
- Higher build and packaging cost
- More migration work before another rehearsal

## Recommendation

Use Option A. It is the simplest architecture that meets the actual one-host, one-profile transaction requirement. Rebuild a new preseal only after:

1. Binding all operational target fields into the approval digest.
2. Freezing the complete transitive controller source.
3. Making verifier and executor consume the same package schema.
4. Pinning `--preseal` in the public entrypoint.
5. Comparing live registry/protected bytes against sealed hashes before reservation.
6. Enforcing exact service identity, ExecStart argv, and restart freshness.

No files, authority state, services, providers, network operations, or Telegram surfaces were modified or invoked.