feat(launcher): prepare Campaign LA11 user gate

This commit is contained in:
Erik 2026-08-14 23:09:08 +02:00
parent 09d84387a8
commit f881e5b467
24 changed files with 3538 additions and 58 deletions

View file

@ -0,0 +1,571 @@
# Campaign LA11 — automated preflight and connected user gate
**Status:** implementation checkpoint only. Run this script after the reviewed
LA10/LA11 commits are integrated and the campaign branch is clean. Campaign LA,
the Linux graphical client, and issue #397 remain open until the user records a
verdict for every applicable row below.
This is the single Campaign LA operator script. The automated section is
display-free and connection-free. Rows AI are deliberately manual and serial:
they use real retail DATs, a local ACE server, user-entered credentials, and
visual judgment that automation cannot supply.
## 1. Safety boundary and required inputs
Use placeholders throughout; never paste a password into a terminal, this
document, a screenshot, or a gate report.
- `<ABSOLUTE_REPOSITORY_ROOT>`: a clean Campaign LA worktree at the exact commit
under test.
- `<ABSOLUTE_RETAIL_DAT_DIRECTORY>`: a read-only source containing
`client_portal.dat`, `client_cell_1.dat`, `client_highres.dat`, and
`client_local_English.dat`.
- `<ACE_PORT>`, `<LA11_SERVER>`, and `<LA11_ACCOUNT>`: a local ACE endpoint and
account. Enter the account password only in the launcher's masked Password
field. The launcher intentionally stores it as plaintext in the **isolated**
`launcher-profiles.json`; children receive it through redirected stdin.
- `<OBSERVER_CHARACTER>`: a second user-controlled character that can observe a
private `/tell` from each play mode.
- `<DISPOSABLE_CHARACTER>`: a server-operator-approved disposable character.
Never substitute a primary character. If none exists, provision one with the
local server's normal admin procedure before row G.
- Windows 11 x64, PowerShell 7, .NET 10 SDK, a local ACE server, and a supported
Vulkan Windows machine for rows AH. Ubuntu x64 with PowerShell 7 and a Linux
desktop/WSLg is required for row I. The Avalonia launcher is supported on
Linux; `gui` and `guiSelect` **client** actions must remain disabled with the
Modern Runtime Slice-L explanation.
Close every unrelated `AcDream.App`, `acdream-headless`, and acdream launcher
before starting. Do not run another acdream gate in parallel. All generated
files must stay below one new gate directory; the canonical `%APPDATA%`,
`%LOCALAPPDATA%`, and XDG acdream roots are out of scope.
## 2. Automated preflight — no UI, connection, credential, or bake
In PowerShell 7 on Windows:
```powershell
$Repo = [IO.Path]::GetFullPath('<ABSOLUTE_REPOSITORY_ROOT>')
$Stamp = [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss')
$Gate = Join-Path $Repo "logs/campaign-la-user-gate-$Stamp"
$Preflight = Join-Path $Gate 'automated-preflight'
New-Item -ItemType Directory -Path $Gate | Out-Null
pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') `
-Repository $Repo `
-OutputDirectory $Preflight
$Report = Get-Content -LiteralPath (Join-Path $Preflight 'report.json') -Raw |
ConvertFrom-Json
if (-not $Report.success -or $Report.dirty) {
throw 'Stop: automated preflight failed or recorded a dirty worktree.'
}
if ($Report.head -cne (git -C $Repo rev-parse HEAD).Trim()) {
throw 'Stop: preflight HEAD does not equal the current HEAD.'
}
```
The expected matrix is:
| Platform | Automated command group | Required result | Typical time |
|---|---|---|---:|
| Windows | Release `AcDream.slnx` build, `-m:1` | exit 0 | 515 min |
| Windows | complete Release solution test, serial | exit 0; ordinary known skips only | 2060 min |
| Windows | focused Launcher.Core update tests and launcher update/startup-option tests | exit 0 | 14 min |
| Windows | canonical portable project build/test closure from `headless-portability.yml` | every project exits 0 | 1025 min |
| Windows | self-contained single-file launcher publish for `win-x64` and `linux-x64` | launcher + bake roots present, no root DLL fallback | 310 min |
| Windows | native launcher `--verify-publish` and bake `--help` with bogus `DOTNET_ROOT*` | both exit 0 | <1 min |
| Ubuntu/WSL | run the same helper natively from the Linux path to the worktree | Linux RID report and every row exit 0 | 3590 min |
`report.json` records the tested HEAD/dirty state, OS/RID, exact commands,
durations, exits, redacted logs, and SHA-256/size inventory. A normal preflight
plans 26 commands. It never launches App or Headless in connected mode and
never reads a credential.
### Optional installed-DAT read-only row
This is not a bake and must not replace row A. Add the switches below only when
the DAT directory may be read by tests:
```powershell
pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') `
-Repository $Repo `
-OutputDirectory (Join-Path $Gate 'automated-preflight-with-dat') `
-IncludeInstalledDat `
-InstalledDatDirectory '<ABSOLUTE_RETAIL_DAT_DIRECTORY>'
```
The mandatory installed-DAT result is
`CharacterManagementLiveDatTests` with both `ACDREAM_PROBE_LIVE_MOUNT=1` and
`ACDREAM_DAT_DIR` set inside the child environment. The helper reads the TRX
and fails if the test skipped or did anything other than pass. The action-map
and portal-asset probes are additional coverage, never substitutes. Expected
matrix size: 30 rows.
On Ubuntu/WSL, invoke the same script with native `pwsh`, a Linux repository
path, and a Linux output path. Do not treat a Windows-hosted run over
`wsl.exe` as the Linux row.
## 3. Prepare the deterministic local A/B feed
Build distinct, version-stamped payloads so the staged launcher really changes
from A to B. These commands write only below `$Gate` (normal project `obj/bin`
incremental outputs are the already-authorized build outputs):
```powershell
$VersionA = '1.0.1-la11.a'
$VersionB = '1.0.1-la11.b'
$Payloads = Join-Path $Gate 'update-payloads'
$Fixture = Join-Path $Gate 'update-fixture'
function Publish-LaRelease([string]$Version, [string]$Label) {
$ClientWin = Join-Path $Payloads "$Label/client-win-x64"
$LauncherWin = Join-Path $Payloads "$Label/launcher-win-x64"
$ClientLinux = Join-Path $Payloads "$Label/client-linux-x64"
$LauncherLinux = Join-Path $Payloads "$Label/launcher-linux-x64"
dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') `
-c Release -r win-x64 --self-contained true -p:Version=$Version `
-o $ClientWin --nologo
if ($LASTEXITCODE) { throw "App win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') `
-c Release -r win-x64 --self-contained true -p:Version=$Version `
-o $ClientWin --nologo
if ($LASTEXITCODE) { throw "Headless win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') `
-c Release -r win-x64 --self-contained true -p:PublishSingleFile=true `
-p:Version=$Version -o $LauncherWin --nologo
if ($LASTEXITCODE) { throw "Launcher win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') `
-c Release -r linux-x64 --self-contained true -p:Version=$Version `
-o $ClientLinux --nologo
if ($LASTEXITCODE) { throw "App linux-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') `
-c Release -r linux-x64 --self-contained true -p:Version=$Version `
-o $ClientLinux --nologo
if ($LASTEXITCODE) { throw "Headless linux-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') `
-c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true `
-p:Version=$Version -o $LauncherLinux --nologo
if ($LASTEXITCODE) { throw "Launcher linux-x64 publish failed: $Label" }
}
Publish-LaRelease $VersionA 'A'
Publish-LaRelease $VersionB 'B'
pwsh -NoProfile -File (Join-Path $Repo 'tools/new-campaign-la-update-fixture.ps1') `
-OutputDirectory $Fixture `
-ClientWinX64DirectoryA (Join-Path $Payloads 'A/client-win-x64') `
-LauncherWinX64DirectoryA (Join-Path $Payloads 'A/launcher-win-x64') `
-ClientLinuxX64DirectoryA (Join-Path $Payloads 'A/client-linux-x64') `
-LauncherLinuxX64DirectoryA (Join-Path $Payloads 'A/launcher-linux-x64') `
-ClientWinX64DirectoryB (Join-Path $Payloads 'B/client-win-x64') `
-LauncherWinX64DirectoryB (Join-Path $Payloads 'B/launcher-win-x64') `
-ClientLinuxX64DirectoryB (Join-Path $Payloads 'B/client-linux-x64') `
-LauncherLinuxX64DirectoryB (Join-Path $Payloads 'B/launcher-linux-x64')
```
The helper rejects nonempty output, invalid or non-monotonic versions, missing
root executables, and nonabsolute inputs. It writes fixed-timestamp sorted ZIPs,
the exact LA10 v1 SHA/size manifest, `fixture-report.json`, a loopback-only
server, and an A/B selector. It does not download or mutate payload sources.
Start the Windows loopback server without a shell or visible helper window:
```powershell
$ServerInfo = [Diagnostics.ProcessStartInfo]::new()
$ServerInfo.FileName = (Get-Command pwsh).Source
$ServerInfo.UseShellExecute = $false
$ServerInfo.CreateNoWindow = $true
foreach ($Value in @(
'-NoProfile', '-File', (Join-Path $Fixture 'serve-fixture.ps1'),
'-Root', $Fixture, '-Port', '43119')) {
$ServerInfo.ArgumentList.Add($Value)
}
$FixtureServer = [Diagnostics.Process]::Start($ServerInfo)
$ManifestUri = 'http://127.0.0.1:43119/manifest.json'
if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionA) {
throw 'Stop: local fixture did not begin on release A.'
}
```
## 4. Windows isolated launcher command and evidence rule
```powershell
$WinRoot = Join-Path $Gate 'windows-roots'
$WinConfig = Join-Path $WinRoot 'config'
$WinData = Join-Path $WinRoot 'data'
$WinCache = Join-Path $WinRoot 'cache'
$Evidence = Join-Path $Gate 'evidence'
New-Item -ItemType Directory -Path $Evidence | Out-Null
$LauncherA = Join-Path $Payloads 'A/launcher-win-x64/acdream-launcher.exe'
$LauncherArguments = @(
'--config-dir', $WinConfig,
'--data-dir', $WinData,
'--cache-dir', $WinCache,
'--update-manifest-uri', $ManifestUri)
& $LauncherA @LauncherArguments
```
All four options are process-local. The three roots are an indivisible set;
the local feed reaches only the updater and is never persisted. A self-update
must preserve the same validated suffix through helper and confirmation
restarts. The launcher, profiles, installer, current-version store, updater,
session composer, and orchestrator must all use this one exact path set.
For every play/probe row, copy the session id shown in the launcher's Sessions
list into `<SESSION_ID>`, then run:
```powershell
$Status = Join-Path $WinCache 'launcher/sessions/<SESSION_ID>/status.jsonl'
pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') `
-StatusFile $Status `
-Mode '<probe|guiSelect|gui|headless>' `
-ExpectedSessionId '<SESSION_ID>' `
-ReportPath (Join-Path $Evidence '<ROW>-status.validation.json')
```
Add `-ExpectedPlugin acdream.smoke` to rows DF. The validator enforces exact
v1 fields **and property order**, one session id, UTC monotonic timestamps,
mode-specific lifecycle order, exit code 0/reason, no unexpected plugin/login
command failure, credential redaction, and no surviving App/Headless process.
Its report contains event names and a hash, not account, character, command, or
error payloads. Keep raw `session.json`/`status.jsonl` local; never upload them.
## 5. Serial Windows user rows AH
### A — isolated first run, real DAT bake, and release-A client baseline
1. Confirm the launcher opens First-run setup and all launch buttons are
unavailable. Save a redacted screenshot as `A-first-run-required.png`.
2. Enter `<ABSOLUTE_RETAIL_DAT_DIRECTORY>` in the wizard, select a sensible
worker count, and click **Validate**. Confirm all four DATs pass.
3. Click **Build and install**. Do not cancel or close the launcher. The real
bake may take 30180 minutes. Confirm every phase reaches **Completed** and
the status says `Client content installed and verified. Launch is enabled.`
4. Open **Check for updates**. Confirm available release A, click **Install
client**, and wait for `Client update installed and activated.` Do not stage
launcher A; the test launcher already has version A.
5. Confirm these exact isolated artifacts exist and no `.previous-install`
remains after success:
```powershell
$RequiredA = @(
(Join-Path $WinData 'install.json'),
(Join-Path $WinData 'pak/acdream.pak'),
(Join-Path $WinData 'app/current.json'))
foreach ($Path in $RequiredA) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Missing $Path" }
}
$RequiredA | ForEach-Object {
$Item = Get-Item -LiteralPath $_
[ordered]@{
name = $Item.Name
size = $Item.Length
sha256 = (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash.ToLowerInvariant()
}
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'A-install-hashes.json')
```
Do not copy `install.json` into shared evidence because it records the local DAT
path. Expected time: 45200 minutes including bake.
### B — server/account CRUD entirely through the UI
1. Add `<LA11_TEMP_SERVER>` at `127.0.0.1:<UNUSED_LOCAL_PORT>`, edit its name
and port, then remove it. Confirm Cancel/Escape makes no mutation.
2. Add `<LA11_SERVER>` at `127.0.0.1:<ACE_PORT>`.
3. Under it add `<LA11_TEMP_ACCOUNT>` with a user-invented throwaway field
value, edit its account name/value, then remove it. Do not reuse a real
password for this temporary row.
4. Add `<LA11_ACCOUNT>` and enter its real password only in the masked field.
5. Close and reopen the launcher with the **same** `$LauncherArguments`. Confirm
only the real server/account persisted. Save redacted before/reopen images as
`B-crud-before-reopen.png` and `B-crud-after-reopen.png`.
6. Record only the profile file's size/hash, never its contents:
```powershell
$Profile = Join-Path $WinConfig 'launcher-profiles.json'
$Item = Get-Item -LiteralPath $Profile
[ordered]@{
size = $Item.Length
sha256 = (Get-FileHash -LiteralPath $Profile -Algorithm SHA256).Hash.ToLowerInvariant()
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'B-profile-hash.json')
```
Expected time: 1015 minutes.
### C — live character probe twice, no stale ACE session
1. Select `<LA11_ACCOUNT>`, click **Refresh characters**, and wait for the
probe row to finish. Confirm the roster appears without entering world.
2. Run the validator in `probe` mode for its session id. Confirm its exact
event order is `started, connected, characterList, disconnected, exited`,
with no `enteredWorld`, terminal code 0, and terminal reason `probe`.
3. In the ACE console/session administration view, confirm the account is no
longer logged in. Save a redacted `C-probe-1-ace-cleared.png`.
4. Repeat steps 13 immediately, producing a different session id,
`C-probe-2-status.validation.json`, and `C-probe-2-ace-cleared.png`.
5. Confirm Refresh is re-enabled and no `acdream-headless` process remains.
Expected time: 510 minutes. A stale ACE account or timeout is a gate failure;
do not wait three minutes and call the next attempt a pass.
### D — `guiSelect`, retail character screen, plugin, and login command
1. Select one non-disposable roster character. Set its mode to `guiSelect`,
Plugins to exactly `acdream.smoke`, and its one login command to
`/tell <OBSERVER_CHARACTER>, LA11-D-<UNIQUE_NONSECRET_NONCE>`. Save settings.
2. Click **GUI — character select**. Confirm the flat retail character list,
selection highlight, Enter button, Delete/Restore swap state, dialogs, and
absence of any invented rotating 3D preview. Save redacted
`D-character-select.png`.
3. Select the configured character and enter world. Confirm the observer gets
the exact D nonce once. Save `D-observer-tell.png` with names redacted.
4. Click **Stop** in the launcher. Confirm the game closes gracefully and ACE
releases the account. Validate `guiSelect` with
`-ExpectedPlugin acdream.smoke`.
Expected time: 510 minutes.
### E — direct `gui`, plugin, and login command
1. Change the same character to `gui`, retain `acdream.smoke`, and change the
command nonce to `LA11-E-<UNIQUE_NONSECRET_NONCE>`.
2. Click **GUI — enter world**. Confirm it selects the exact cached character,
reaches the world, loads the plugin once, and the observer gets the E nonce
once.
3. Stop from the launcher, confirm ACE logout, and validate `gui` with the
expected plugin. Save `E-world.png`, `E-observer-tell.png`, and
`E-status.validation.json` with identifying text redacted.
Expected time: 510 minutes.
### F — headless, plugin/login command, and connected #397 acceptance
1. Change the same character to `headless`, retain `acdream.smoke`, and use
`LA11-F-<UNIQUE_NONSECRET_NONCE>`.
2. Click **Headless**. Confirm `pluginLoaded(acdream.smoke)`, `enteredWorld`,
and the observer's single exact F nonce.
3. Click **Stop** once. On Windows this must target that child's distinct
process group with `CTRL_BREAK`; it must reach `disconnected` then
`exited(code:0, reason:graceful)` before the timeout, without a hard kill.
ACE must release the account immediately and the launcher must stay open.
4. Validate `headless` with the expected plugin and save
`F-status.validation.json` plus redacted ACE-clear evidence.
The real automated fixture separately proves complex argv and redirected stdin
survive native `CreateProcessW`, the target receives `CTRL_BREAK`, a sibling
process group receives nothing, exit 0 precedes timeout, and `Kill` is never
called. This connected row proves the actual ACE graceful-logout half. Issue
#397 remains open if either half is missing. Expected time: 510 minutes.
### G — disposable delete and restore
1. Launch `guiSelect` for `<DISPOSABLE_CHARACTER>`. Do not enter world.
2. Confirm ordinary selection enables Enter/Delete and disables Restore. Click
Delete, inspect the retail confirmation dialog, cancel once, and confirm no
state change.
3. Delete again and confirm. Verify the wait dialog, greyed roster row/countdown,
disabled Enter/Delete, and enabled Restore. Save `G-deleted.png`.
4. Click Restore and confirm the same GUID returns to ordinary state with
Enter/Delete enabled and Restore disabled. Save `G-restored.png`.
5. Close through launcher **Stop**, confirm graceful terminal status and ACE
release. Validate with:
```powershell
pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') `
-StatusFile (Join-Path $WinCache 'launcher/sessions/<SESSION_ID>/status.jsonl') `
-Mode guiSelect `
-ExpectNoEnteredWorld `
-ExpectedSessionId '<SESSION_ID>' `
-ExpectedPlugin acdream.smoke `
-ReportPath (Join-Path $Evidence 'G-status.validation.json')
```
If restore fails, stop the row, preserve evidence, and restore only that
disposable character with the server's normal admin recovery. Never continue
with another character. Expected time: 510 minutes.
### H — local A→B client update, active-session refusal, rollback, self-update
1. Record release A's `app/current.json`. Start one headless session and wait
for `enteredWorld`.
2. Switch the fixture atomically to B:
```powershell
pwsh -NoProfile -File (Join-Path $Fixture 'set-active-release.ps1') `
-Release B -Root $Fixture
if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionB) {
throw 'Stop: fixture did not switch to B.'
}
```
3. Open **Check for updates** and **Check again**. While the session is active,
confirm Install client, Rollback client, and Stage launcher are disabled or
refuse without changing `app/current.json`. Save `H-active-refusal.png`.
4. Stop the headless session and validate its graceful status. Install client
B. Confirm `app/current.json` names B, A is previous, all installed-file
hashes verify, and new sessions resolve from the B directory.
5. Click **Rollback client**. Confirm A becomes current and B becomes previous.
Check again and install B once more, leaving B current. Save sanitized copies
of the three pointer states as `H-pointer-a.json`, `H-pointer-b.json`, and
`H-pointer-rollback-a.json`; they contain no credentials.
6. Click **Stage launcher**. Confirm restart is required, then close the
launcher normally. The copied helper must apply B and restart the launcher
with the same config/data/cache/feed suffix.
7. Confirm profiles, install record, and update state still come from the
isolated roots; `campaign-la-fixture-release.txt` beside the relaunched
executable says `release=B`; `launcher-update/pending.json` is gone; and no
transaction backup remains. Check again and confirm launcher B is current.
Save `H-self-update-confirmed.png` and a hash-only post-state inventory.
Never edit a manifest to force this row and never point the launcher at a
non-loopback HTTP endpoint. Expected time: 1530 minutes.
## 6. Row I — native Ubuntu/WSL launcher, XDG-shaped isolated roots
Stop the Windows launcher and fixture server only after every Windows session
is terminal:
```powershell
if (-not $FixtureServer.HasExited) {
$FixtureServer.Kill()
$FixtureServer.WaitForExit()
}
```
In a native Ubuntu/WSL PowerShell 7 terminal, set Linux paths. The repository
and fixture may be read from a mounted Windows path, but roots must live on the
Linux filesystem. Run the generated server natively so its `127.0.0.1` URLs
cannot escape the Linux environment:
```powershell
$RepoLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_REPOSITORY_PATH>')
$FixtureLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_FIXTURE_PATH>')
$PayloadsLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_PAYLOADS_PATH>')
$LinuxGate = [IO.Path]::GetFullPath('<NEW_ABSOLUTE_LINUX_GATE_ROOT>')
$env:XDG_CONFIG_HOME = Join-Path $LinuxGate 'xdg-config-home'
$env:XDG_DATA_HOME = Join-Path $LinuxGate 'xdg-data-home'
$env:XDG_CACHE_HOME = Join-Path $LinuxGate 'xdg-cache-home'
$LinuxConfig = Join-Path $env:XDG_CONFIG_HOME 'acdream'
$LinuxData = Join-Path $env:XDG_DATA_HOME 'acdream'
$LinuxCache = Join-Path $env:XDG_CACHE_HOME 'acdream'
$LinuxEvidence = Join-Path $LinuxGate 'evidence'
New-Item -ItemType Directory -Path $LinuxEvidence | Out-Null
pwsh -NoProfile -File (Join-Path $FixtureLinux 'set-active-release.ps1') `
-Release A -Root $FixtureLinux
```
Start `serve-fixture.ps1 -Root $FixtureLinux -Port 43119` in a dedicated native
terminal and leave it running. In another terminal:
```powershell
$LauncherLinuxA = Join-Path $PayloadsLinux 'A/launcher-linux-x64/acdream-launcher'
& $LauncherLinuxA `
--config-dir $LinuxConfig `
--data-dir $LinuxData `
--cache-dir $LinuxCache `
--update-manifest-uri 'http://127.0.0.1:43119/manifest.json'
```
Complete this exact serial matrix:
1. **Manual-DAT first run:** enter `<ABSOLUTE_LINUX_RETAIL_DAT_DIRECTORY>`;
auto-detection may be empty by design. Validate, bake to
`$LinuxData/pak/acdream.pak`, verify, then install release-A client.
2. **CRUD:** add/edit/remove a temporary server and account entirely in the
launcher, then add the real Linux-reachable ACE profile. Enter its password
only in the masked field. Restart and confirm persistence. Run
`stat -c '%a' "$LinuxConfig/launcher-profiles.json"`; the exact result must
be `600`.
3. **Probe twice:** run Refresh twice, validate both status streams in `probe`
mode with native `pwsh`, and confirm ACE clears the account after each.
4. **Platform posture:** confirm GUI and GUI-select client buttons are disabled
and show the explicit Modern Runtime Slice-L message. Do not bypass this
disablement and do not claim a Linux graphical-client gate.
5. **Headless:** configure `acdream.smoke` and
`/tell <OBSERVER_CHARACTER>, LA11-I-<UNIQUE_NONSECRET_NONCE>`, launch, observe
the tell, click Stop, and validate `headless` + expected plugin. Native Linux
sends SIGINT and must reach graceful terminal status with no process leak.
6. **Update:** switch the native fixture to B, prove update actions refuse while
a headless session is active, stop it gracefully, install B, rollback to A,
reinstall B, stage launcher B, and close normally. Confirm the relaunched
binary's B marker, preserved explicit roots/feed, cleaned pending journal,
and executable owner bits on App, Headless, Launcher, and Bake.
Copy only redacted screenshots, validation reports, pointer JSON, hashes, and
file-mode results into `$LinuxEvidence`. Keep the Linux profile and raw session
files local. Expected time: 60220 minutes, dominated by the real bake.
## 7. Evidence, redaction, verdict, and cleanup
Expected evidence tree:
```text
logs/campaign-la-user-gate-<timestamp>/
automated-preflight/report.json
automated-preflight/commands/*.log
automated-preflight/publish/{win-x64,linux-x64}/...
update-fixture/fixture-report.json
update-fixture/{A,B}/manifest.json
evidence/A-install-hashes.json
evidence/B-*.png
evidence/C-probe-{1,2}-status.validation.json
evidence/D-*.png + D-status.validation.json
evidence/E-*.png + E-status.validation.json
evidence/F-*.png + F-status.validation.json
evidence/G-*.png + G-status.validation.json
evidence/H-*.png + H-pointer-*.json
evidence/I-*.png + I-status.validation.json + I-modes.txt
verdict.json
```
Before sharing evidence:
- remove or mask account names, character names, DAT paths, hostnames other than
loopback, and server-admin identifiers from screenshots;
- never copy `launcher-profiles.json`, raw session configs/status streams,
stdout/stderr that may contain user text, or environment values;
- search the shareable evidence for the exact user-entered password and any
gate-only sentinel secret; the match count must be zero;
- retain SHA-256 and sizes so local raw artifacts remain auditable.
No additional raw child/plugin diagnostic sink is required: `pluginLoaded`,
the strict terminal status, the observer's redacted tell evidence, and the
automated targeted-signal fixture cover the acceptance questions without
capturing credentials or arbitrary chat.
Create `verdict.json` manually with schema version 1, exact tested HEAD, rows
AI as `pass`, `fail`, or `notApplicable`, a short redacted note per row, and
the user's overall verdict. Row I is not applicable only when no native
Ubuntu/WSL desktop is available; it blocks Campaign LA shipping under the
current Linux requirement, so it cannot be silently omitted.
Cleanup is graceful-first and serial:
1. Restore `<DISPOSABLE_CHARACTER>` and verify it is ordinary before closing
its session.
2. Stop every launcher session once; require a passing validator and ACE-clear
observation. If a child survives the timeout, record the gate failure and
its PID before any emergency termination.
3. Close each launcher normally, then stop only the fixture-server process
created above. Do not kill ACE as a substitute for logout evidence.
4. Leave update pointers on B or roll the **isolated** client back to A through
the UI; never edit pointers or journals by hand.
5. Remove the real account through the isolated launcher UI. After review,
delete only the explicitly recorded `$WinConfig`/`$LinuxConfig` gate roots
that held plaintext passwords, or change the test account password. Do not
recursively delete a computed, empty, canonical, home, repository, or XDG
parent path.
6. Preserve the redacted evidence and reports. The large isolated pak/payload
trees may be removed only after resolving and checking their full paths are
descendants of the recorded gate roots.
Estimated total: 37 hours, primarily the two real DAT bakes and full serial
test suites. A failure stops the current row; restore/stop/collect evidence,
then diagnose before advancing. Do not mark LA11, Campaign LA, or #397 shipped
until the user accepts the complete applicable matrix.