Repository navigation
Feat/proxy - #97
Draft
joshooaj wants to merge 195 commits into
Draft
Feat/proxy#97joshooaj wants to merge 195 commits into
joshooaj wants to merge 195 commits into
Conversation
Admins can enable a per-workspace proxy and allowlist proxy targets (scheme/host/port) in Management Client; settings are stored server-side in the workspace JSON blob, default to proxy-off for definitions saved before the proxy existed, and travel to the Smart Client view item properties where the properties page reports proxy state and allowlisted targets. Adds a pure ProxyPolicy (canonicalization + exact-origin request matching) registered in both plugin and test compile inventories.
Register the proxy area in the verification matrix (MC-9); preserve allowlisted targets when the admin toggles the proxy off instead of wiping them; funnel both canonical-origin paths through one FormatOrigin formatter and one dedup shape; add a Proxy_Off_Preserves_Stored_Targets pin.
… GET (#53) plugin.def gains the Service load environment; a Service-targeted BackgroundPlugin answers WebContent.Proxy.FetchRequest, performs the GET under connect/read/size budgets (ProxyHttpFetcher), and streams the outcome back as meta + base64 chunk frames to ProxyFetchClient. Management Client workspace page gains the AC5 demo harness (Test proxy fetch). Pure seams (contract, framer, correlator, transport) dual-registered in both csprojs; 30 new tests pin framing, correlation, transport bounds, payload whitelisting, id-suffix rules, and Service load. Correlator hardening: garbled/hostile frames complete with an error outcome instead of throwing on the receiver thread; no MessageCommunicationManager.Stop (process-wide session shared with other plugins, per MIP sample precedent).
…ng MC on display (#53) OnProxyTestFetch awaited with ConfigureAwait(false), so the result display and the finally block ran on a thread-pool thread; the WPF access threw Dispatcher.VerifyAccess and took down VideoOS.Administration. WPF event handlers must not ConfigureAwait(false): the continuation now resumes on the UI thread and the manual Dispatcher.Invoke re-dispatch is gone.
…ved by the Event Server fetcher (#54) - LoopbackProxyListener: one ephemeral 127.0.0.1 listener per proxied workspace browser profile; allowlist policy (403 off-list, 405 non-GET, 502 CONNECT, 400 malformed); https pinned with CONNECT refused - never direct. - ProxyBrowserArguments pins every scheme to the listener and disables the <-loopback bypass; proxied profiles get their own WebView2 user-data dir. - ProxyForwardCodec/Policy/Connection + ProxyWorkspaceProfile + ProxyServerAllowlist as pure tested seams. - Event Server fetcher hardening: server-side allowlist union over proxied workspaces (closed before first resolve, stale-on-error). - Round-trip fix (live-host diagnosis): responses are unicasted to the requester via Message.ExternalMessageSourceEndPoint. The receive-callback source parameter is null for our requests, and answering to a null destination filter-routed frames to every FetchResponseId subscriber: refusals vanished (both clients timed out) and fetched bodies leaked to unrelated hosts (strange ProxyFetchResponsePayload deserialization errors). Pinned by ProxyFetchReplyGuardTests. - MC-10 probe (launch+login FlaUI) verified live on BEAST: non-allowlisted origin -> NotAllowed 0.0s; allowlisted https://joshooaj.com -> HTTP 200, 37521 bytes, 0.3s. ES log gains the matching refusal marker.
…cross the wire
Smart Client timed out every proxied page load while the Event Server answered
in ~70 ms. Root cause: the platform rehydrates ExternalMessage.Data via
"new DataContractSerializer(Type.GetType(DataClass))" (ExternalMessage.cs). In
the Smart Client host Type.GetType cannot see WebContent.dll, returns null, the
platform swallows the ArgumentNullException ("Value cannot be null", logged as
"ExtenalMessage.Deserialize ():Unable to deserialize class:...") and delivers
Data == null to the receive callback - a silent black hole. Management Client
and the Event Server resolve plugin types fine, which is why only Smart Client
failed. Proven with the trace-gated request-echo filter: even the client's own
transmitted FetchRequest echoed back with null Data.
Fix: ProxyFetchWireCodec serializes every wire payload to a plain JSON string
(System.String binds in every host); encode at all four transmits, decode at
both receives. Codec file registered in WebContent.csproj and the
WebContent.Tests.csproj Compile link.
Also in this change:
- ProxyFetchCorrelation.Feed returns whether the frame owned a pending
exchange (powers the unmatched-frame trace); unit-pinned.
- Trace-gated client diagnostics (WEBCONTENT_MIP_TRACE=1): not-recognized /
unmatched / request-echo logs; Close() detaches both filters.
- ScProxyFetchProbeTests: FlaUI Smart Client probe (Category=probe) with the
SC-1 negative control (WEBCONTENT_SC1_BLOCKED=1 logs a direct client-side
GET that must fail while the proxied page renders).
- McProxyFetchProbeTests case 2 moved to http://httpforever.com/.
- Verification matrix: ES-1 bullet 3 rewritten (JSON-string wire, no payload
whitelist rationale), SC-1 harness + pass record, new regression-table row
pinning JSON-string payloads.
Live proof (drop AF65D193...7FF9 on both machines): SC probe rendered
'httpforever' (title + 2103 body chars) with zero deserialize/timeout lines;
blocked-control run rendered identically while the same process's direct GET
was refused - traffic originates on the Event Server. MC-10 regression on the
codec build: NotAllowed 0.1s + HTTP 200 11513 bytes. Unit suite: 309 pass.
Refs #54
…ring wire, SC-1 probe asserts (route-block pre-check, error-page/body-size red), codec thread-safety note, matrix row drift
…d body; cookie-pure passthrough (#55) The loopback forwarder stops being GET-only: browser login POSTs and background XHR cross the MIP wire via ProxyFetchRequestPayload {Method, Headers, BodyBase64} - still ONE JSON string per #54, never plugin types. New ProxyFetchQuery seam carries uri/method/headers/body through listener -> BrowserManager -> ProxyFetchClient and into ProxyHttpFetcher. - decision table: GET/HEAD/POST/PUT/DELETE/OPTIONS/PATCH forward, TRACE and junk verbs 405, CONNECT stays 502; ES re-checks the same shared IsForwardedMethod table against decoded payloads - request bodies read per Content-Length (1 MiB cap -> 413 before any bytes are taken; chunked -> 411; short body -> 400) - outbound headers: Cookie/Authorization/Content-Type/custom pass verbatim; hop-by-hop + Host + Content-Length dropped (client AND transport both filter, so a crafted payload can't restate them) - session purity: AllowAutoRedirect off (3xx+Location relayed), no cookie store on the VMS server (UseCookies off via guarded set - the property is absent on compat shims); repeated Set-Cookie captured per raw value so the comma-join can't corrupt the session
…nnot throw, wire values CR/LF-gated; Event Server re-caps request body size; source guards pin request-side payload assembly and ES method/base64/cap re-checks; stale-comment and version-skew doc corrections
…bodies, not HTML-wrapped, so #xhr-status asserts the exact payload the fetcher carried
…R to resolve before reading #xhr-status (XHR-PENDING placeholder race); Management Client tree search gets a grace period while the tree populates after login
…king - EnsureExpanded enumerates realized rows, double-clicks the target, and wheel-scrolls for below-the-fold rows/children (geometry expansion check); AcceptCertificateTrust exits as soon as the client main window is up instead of polling its full timeout; SC-2 probe collapses to root (fast double-click route) then walks the plugin chain; SelectNode sleeps tightened
… through the proxy passes on drop 6C373E4D (both machines, 2026-10-31)
…ult, dedicated TlsValidation refusal with actionable message, per-request (never ServicePointManager) override hook; ADR-0002 records the loopback-termination design
…rtificate override - union snapshot splits flagged origins, Event Server scopes the certificate mode to the exact allowlisted origin, config member acceptUntrusted defaults absent=false
…t-untrusted flag token through parse/save/load roundtrip (ProxyPolicy.TryParseTargetEntry/DescribeTargetEntry), MC page hint explains the opt-in
… https origins terminate the tunnel at the loopback listener (ADR-0002), other CONNECTs get the 403 origin posture; codec resolves the authority to its https origin
…ts resolved against the listener-terminated tunnel origin; absolute-form smuggling and nested CONNECTs are refused
…ls - 200 handshake, inner request loop against the tunnel origin through the decision/fetch/render pipeline, pipelined remainder bytes carried across requests
…ves - Framework path hand-builds DER over persisted CAPI keys (CertificateRequest absent on 4.7.2), modern path uses CertificateRequest with PFX roundtrip; leaves survive real Schannel server handshakes (net472 proof: compiled plugin DLL + PS 5.1 clients, incl. seeded AcceptTunnelAsync with 32-byte pipelined ClientHello fragment)
…roxyLoopbackTls per proxied browser, session CA trusted in the user root store only while https targets are allowlisted (installed on start/target edit, removed on stop/target strip), terminated tunnels disposed with the request loop
…xed deployment CA created by the Event Server in LocalMachine\\My, leaves minted server-side over a LeafRequest carrying the client's SPKI, allowlist is the minting policy (no name constraints - static subtrees cannot match runtime config), clients trust the CA DER via one-time elevated Import-Certificate into LocalMachine\\Root
…the client's SPKI only, the workspace allowlist union is the minting gate (NotAllowed fail-closed), Authority.MintLeafForPublicKey signs client public keys on both TFM backends, single-frame leaf correlation with deadline, NoAuthority error for an unloadable deployment CA
…saging plugin (ADR-0003) - unicast leaf replies (never broadcast), deployment CA lazily loaded/created in LocalMachine\\Personal under a fixed machine key container with per-request retry until provisioned
…erates the per-origin exchange key (CAPI AT_KEYEXCHANGE container on Framework, PFX roundtrip on modern .NET), ProxyLoopbackTls mints through the LeafRequest channel over ProxyFetchClient and reports LocalMachine\\Root trust posture once per CA thumbprint, listener no longer touches any certificate store (read-only ProxyLoopbackTrust replaces install/remove)
…PS mode (in-process cert, SAN for the VMS-network address, TLS journal entries as probe data) and the login probe splits into fail-closed (proxy error page + per-target guidance, refused handshake journaled server-side) and accept-untrusted (full login/session/XHR through the tunnel) runner-shared facts
…o LocalMachine\\Root, uninstall cleanup) + verification-matrix SC-3 row (fail-closed / accept-untrusted live rows, unit pins, live net472 plumbing proof recorded)
… modal - UIA Invoke blocks for the whole modal lifetime (the hang a human had to click through), so Save clicks on a side thread while the harness sweeps modals, verifies the proxy-target readback after save, and retires MC (close + modal sweep + kill) instead of leaving it open for the Smart Client phase
Live Event Server installs run MilestoneEventServerService as NT AUTHORITY\NETWORK SERVICE, which cannot create the deployment CA in LocalMachine\Personal or read its machine key. One-time elevated setup (script) now creates the CA, grants the service account read access to the private key, and exports the certificate for client LocalMachine\Root import; README and ADR-0003 record the corrected lifecycle.
…e the machine store Live Event Server runs as NT AUTHORITY\NETWORK SERVICE, which has only ReadKey on the HKLM My certificate store: X509Store.Open(ReadWrite) throws Access is denied before the existing CA is ever found, so every leaf request answered NoAuthority even after provisioning. The store is now opened read-write first (create-when-allowed) and otherwise read-only for the admin-provisioned CA; a missing CA in read-only mode throws a message naming the one-time elevated provisioning script, which surfaces through NoAuthority into both logs. Probe harness now logs the full stack and flattened AggregateException inners instead of an empty One or more errors occurred. ()
…aggregate, and the explicit-contract skew fix - ProxyDiagnosticsAggregatorTests rows (per-machine dedup incl case-insensitivity + count accumulation + recency refresh, per-workspace tables + blank-machine drop, 200-cap least-recently-seen eviction, RecordBlockForSession never touches the machine table, the table survives the idle sweep like the blocked rings); ProxyDiagnosticsWireTests round trip plus the pre-#80 frame pin: DataContractJsonSerializer REFUSES a missing member on an inferred contract (proved by scratch repro: required data member not found), so ProxyStatusResponsePayload moved to explicit [DataContract]/[DataMember] like ProxyFetchRequestPayload (#65 rule) - frames predating UntrustedMachines decode with the list null instead of dying; ProxyFetchContractGuardTests whitelists ProxyCaUntrustedMachineEntry; ProxyCaTrustStateTests LastMintOrigin pins (quiet refresh, never change-detection, blank origin keeps last) + ProxyLoopbackTlsTrustPostureTests mint cacheKey reaches the posture; ProxyDiagnosticsViewTests machine rows (pre-formatted, newest-first, blank drops, missing list reads zero, rows render with an empty blocked ring); ProxyCaUntrustedReportWiringGuardTests source pins for all three surfaces (listener subscribe/unsubscribe pair, IsUntrustedConfirmed gate + https://+LastMintOrigin + _blocks never _tally, ES attribution gate + single RecordUntrustedAuthorityReport call site + the once-per-machine log marker, Diagnostics banner Collapsed start + probe ids + shared export sentence + single flight, resx wording pins); aggregator blank guard widened to IsNullOrWhiteSpace; TestStrings stub gained the three members the now-linked ProxyCaExportFailureSentence needs; WorkspaceTabShellGuardTests Diagnostics pin allows exactly btnCaUntrustedExport->OnCaUntrustedExport (read-only export, arms no Save); 1079 unit tests green, Lint green
…; promote probe fix (#80, #84) McCaUntrustedAggregateProbeTests: row 1 (banner absent while the CA is trusted) proven live green; row 2 (flip trust, report, banner names the machine, inline export ships the live leaf, promote restores) honest-skips unattended on the store surgery, with the elevated human step documented in the matrix. MC-16 promote probe: count the marker baseline with the SAME origin filter as the wait (the old baseline counted other origins' promotions and poisoned the delta) and re-check the marker between promote attempts. README: the aggregate warning + its inline export belong to the CA story. Co-Authored-By: Claude <noreply@anthropic.com>
…egate untrusted-CA reports from client machines: clients whose proxied https view hits the untrusted-deployment-CA condition file ONE CertUntrusted report per flip through the existing WebContent.Proxy.BlockReport channel (ClientMachine attribution + mint-time origin; never the #83 setup tally); the Event Server keeps an in-memory per-MACHINE table per workspace (case-insensitive dedup, count + last-seen, 200-cap, idle sweep never clears it, restart drops it - no persistence by design) and answers the aggregate with the StatusRequest snapshot the blocked ring rides (explicit [DataContract] payload so pre-#80 frames still decode); MC Diagnostics shows a role-gated amber banner - 'N client machines report the proxy CA is not trusted. Export it from here and distribute it.' - naming each machine with the #75 export button inline. ES-decided target-chain failures NEVER attribute a machine (guard-pinned single call site). 1079 unit tests incl. ProxyCaTrustAggregationTests + wire skew pins; MC-20 probes: row 1 (no banner while trusted) live PASSED 2026-11-03 run 20261103-075140, row 2 (flip trust -> report -> banner names the machine -> inline export ships the live leaf -> promote heals) honest-skips unattended with the elevated human step documented; MC-16 promote probe fixed on the side (origin-filtered marker baseline + retry loop) and re-proven live
…ew WebContentServiceCtl x64 exe streamed into the package as Binary and run as two deferred exe CAs (SYSTEM, Return=check): StopEventServerService at 1502 (after the #70 kill scan, before any file copy) waits for the real Stopped state - no fixed sleep; RestartEventServerService after InstallFiles starts the service again when the finish-page choice is Yes. Stop+restart arm ONLY when ES is present AND the plugin is already installed (WIX_UPGRADE_DETECTED via the widened UpgradeVersion 0.0.0-1.0.0 range + FileSearch probe of the drop for Copy-Item-deployed labs); first-time install never stops the service (acceptance: stop/restart/prompt absent); machines without ES see nothing. RESTART_EVENT_SERVER public property defaults to 1 - RESTART_EVENT_SERVER=0 declines for scripted installs. UI: WixUI_Minimal forked in-package (WebContentMinimal + WebContentWelcomeEulaDlg per the documented fork recipe) + new EventServerOptionsDlg pre-install options page with the Restart the Event Server now? checkbox (checkbox -> EventServerOptions property -> SetProperty, since post-InstallFinalize CAs cannot be driven by finish dialogs); the warning text spells out the MessageCommunication consequence: every connected Smart Client must be restarted afterwards. ServiceCtl exit codes 0 done-or-absent / 1 usage / 2 wait-timeout (StopWaitTimeout 3min, StartWaitTimeout 2min); pure ServiceCtlCore seam + 13 unit rows + 12 WiX wiring guard rows; build.ps1 structural guard pins sequences, CA type/binary, condition needles, and the checkbox ControlEvents. 1113 unit tests, Lint green
…cation-matrix IN-2 (five live rows PASSED 2026-11-03 on the VMS machine with 250 ms service-state journals: upgrade-Yes stop->copy->start, upgrade-No left Stopped, first-install never stopped + start-only-on-Yes, uninstall untouched with the stop-service-first caveat, no-ES machine both actions skipped), feature + regression rows, and scripts/manual-installer-ui-row.ps1 for the human UIA-on-msiexec walkthrough of the choice page
…ered indicator label VideoOSButtonTransparentSmall is icon-only: its template never presents text Content, so the 'N resources blocked' TextBlock authored as the button's content rendered nothing and surfaced no UIA peer (run 20261103-102550: blockedIndicatorText read as missing, screenshots showed an anonymous icon). The label is now a toolbar SIBLING TextBlock with an explicit AutomationProperties.AutomationId, the button carries a Warning_Combined icon, and UpdateBlockedIndicator toggles both in lockstep; ProxyBlockIndicatorWiringGuardTests pins the sibling shape, icon, id, lockstep and the setup-mode gate. Probe restate: rows are ENUMERATED (FindAllDescendants paired by tree order) so a browser-service row can never mask the probe origin, and the clean-page reset contract is ORIGIN-scoped - the pinned browser proxies Edge's own service traffic (nav-edge.smartscreen... rides every navigation, refuses NotAllowed, re-tallys within seconds), so 'the whole indicator vanishes' was the wrong expectation. SC-7 PASSED 20261103-104747 (drop 972A01EA...09F78E03 hash-verified): label renders '4 resources blocked', probe origin row count 3, gone after clean navigation with only the browser's own ping kept. ES-2 stale 'not yet run' note refreshed: Proxy block recorded observed live (SC-6 20261102-114705, SC-5 record 7 20261103-020728); session-open proven end-to-end via the MC-12/#76 live session row. 1116 unit tests green, Lint green
…A..., both lab machines now carry the identical-source rebuild BA938406... (v0.1.284.59742)
… (default-ticked restart, upgrade story, warning; hidden unprompted on no-ES box)
…/MC-17/MC-18 re-passed green in ONE 24 m 33 s TestUI run on the merged drop BA938406. (v0.1.284.59742, hash-identical on both lab machines, untouched): wildcard + hatch round-trip with the SSRF floor refusing at 0.0s (admission now proven by the dial - Timeout naming the target - since the public target throttles the 5 s header window), the grid refusal/flag/banner walk unchanged on the merged drop, the cap live in both directions against the raised 20 MB production baseline, and the four forbidden-header cycles with audit lines unmoved 44 -> 44. 1116 unit tests + Lint green
…t path - verified live, Copy-Item demoted to a dev/test shortcut
Live rows on vms.joshooaj.com (matrix IN-3), all against CascadiaWebContentPlugin.0.1.287.14301.msi built from HEAD by build.ps1 (guards green), staged over credential WinRM and hash-verified remotely before every msiexec:
1) Upgrade over the dev-drift state (registered 0.1.282.22971 + Copy-Item-bumped 0.1.284.59742 files): exit 1641, 500 ms journal Running->StopPending->Stopped->StartPending->Running, registration bumped to ONE entry, but hash verification caught a real gap - 12/13 files, the unregistered WebContentTrustHelper.exe stray (file version 1.0.0.0, equal to the payload's) SHADOWED the MSI copy. Registered versioned files replaced fine (0.1.284 -> 0.1.287).
2) Supported migration of a drifted machine, pure msiexec: stop MilestoneEventServerService (documented precondition), msiexec /x {C1D583C8...} /qn exit 0 - registrations 1->0 AND drop files 13->0 (the upgrade had registered the stray, so no manual cleanup is needed), then msiexec /i exit 0 in a true first-install posture (stop action skipped by condition, restart action started the service), ONE registration, DROP HASH VERIFICATION PASSED 13/13, and the Event Server LOADED the MSI drop: 'Proxy fetcher ready (...)' at 12:44:37-08:00, service left Running. ES machine now current purely by MSI.
Docs: README server-side section restructured (MSI there = supported, exact silent commands, drift caveat + uninstall/install migration, script/DeployToVms explicitly dev-only and never setting up proxy support); AGENTS.md deploy section states the same posture + the equal-version-stray trap; DP-1 marked superseded-as-path (records stay); deploy-event-server-plugin.ps1 SYNOPSIS/description demoted to dev/test shortcut. Follow-up issue filed for version-stamping the helper so future upgrades self-heal over drifted labs. Remote temp (MSI + both verbose logs + journal) cleaned; Smart Clients connected before 12:44 need the usual restart.
…ll time (ES present + proxy opt-in) - inverts ADR-0003's ordering objection per #64: new EventServerOptionsDlg checkbox 'Enable web content proxy support' (public PROXY_SUPPORT, default 1, scripted decline PROXY_SUPPORT=0) shares the #82 options page; new WebContentProxyCa x64 net472 exe streamed into the package as Binary and run as the deferred CreateProxyAuthority exe CA (SYSTEM, Return=check) ONLY when EVENTSERVERSERVICEPRESENT AND PROXY_SUPPORT=1 - scheduled between InstallFiles and RestartEventServerService so the helper reflects into the just-installed WebContent.dll and the service comes back on a provisioned CA. The helper never mints its own certificate: it drives the plugin's OWN LoadOrCreateDeploymentAuthority by reflection (fixed CN/container, reuse of an existing valid CA untouched, identical shape to the legacy provision-script posture - one recipe, no drift), then grants the Event Server service's own SCM ObjectName read access to the CA private key, locating the key file through the CAPI handle (CspKeyContainerInfo.UniqueKeyContainerName) instead of the script's md5-name guess; grant is idempotent (already-granted = DACL untouched); provision/grant failure = exit 2 = rolled-back install, never a half-set-up deployment. Exit codes 0 done / 1 usage / 2 failed. Machines without ES or with the box unticked never touch LocalMachine\Personal (condition-guarded; RegistrySearch gate same as #82). scripts/provision-event-server-authority.ps1 retired to the legacy/repair fallback (dev Copy-Item labs, hand-repaired stores); plugin lazy first-mint stays the fallback for declined installs; ADR-0003 amended with the supersession map. Tests: ProxyCaProvisionCoreTests decision table (bad-vector refusal before anything is touched, SCM ObjectName account mapping incl LocalSystem/NetworkService normalization, gateway-failure skips the grant, unconfirmed grant fails the install) + InstallerProxyCaGuardTests authored-source pins (CA action/Binary/condition text, restart-after-CA ordering, checkbox on the same page, CN never drifts from ProxyLoopbackTls, gateway never carries its own minting recipe, exe never harvested into the drop) + build.ps1 compiled-package guard (1026 row + binary + command line, sequence slot after InstallFiles / before InstallFinalize / before the restart, condition needles, PROXY_SUPPORT Control row). 1155 unit tests + Lint green
…ver machine for the install-time deployment CA: default-opt-in upgrade 0.1.287->0.1.288 ran CreateProxyAuthority INSIDE the script (deferred 1026 payload CustomActionSchedule at 13:11:21, after the stop's real 3.4 s Stopped wait, before the restart) and the CA came out byte-identical (single CN copy, thumbprint E4D2..., same key container da86e11f..., same three-ACE DACL), one registration, drop hash-verified (helper stale = known #89, re-noted); the package's own exe run directly (hash-matched to the Binary stream) logged the plugin's own 'Loaded deployment proxy CA' + 'already holds read access ... DACL untouched', refused all three bad vectors exit 1, and after NETWORK SERVICE ACE surgery re-WROTE the grant restoring the identical ACL; the 0.1.289 upgrade with PROXY_SUPPORT=0 shows 'Skipping action: CreateProxyAuthority (condition is false)' with no CustomActionSchedule while the service journal runs the normal stop/wait/start; the post-install SC-3 mint probe re-passed (3 m 08 s) - the plugin's own 'Loaded deployment proxy CA (thumbprint E4D2...)' + 'Minted loopback proxy leaf' at 13:27:16 with the provision script never run; BEAST no-ES rows honest-SKIPPED (Medium IL + UAC 5, no local jhadmin, Administrator disabled) with the IN-2-row-5 live precedent for the same EVENTSERVERSERVICEPRESENT gate + compiled-guard pins named in lieu; summary-table row + regression-table row added
…I upgrade IS the provisioning story, live on the Event Server at the merged tip; MC-15 harness fix + IN-5 records - tip 29c4995: Lint+1155 unit rows green, build.ps1 re-green with ALL THREE compiled guards (#70/#82/#77) in one build; the row ran at forced 0.1.291.10692 (IN-2 999.x precedent) because the machine carried an UNRECORDED 0.1.290.10692 registration (installed 13:37, no trail; disclosed, cleaned by the major upgrade) - staged MSI 8186DC89 hash-verified remotely, msiexec /i /qn exit 0 (no 1641 this time, honest note), 500 ms journal Running->StopPending->Stopped 38.1->StartPending->Running 39.1 = 10.1 s REAL Stopped wait with the copy inside the window, log carries WixCloseApplicationsDeferred(3073) then CustomActionSchedule Stop(1026) 17:25:27.553 / CreateProxyAuthority(1026,provision) 17:25:38.348 / Restart(1026) 17:25:38.479, old-product REMOVE script skipped all three (NOT REMOVE); CA byte-identical before/after (one CN CA E4D2..., same container da86e11f..., same three-ACE DACL), single registration, Proxy fetcher ready with export= + triggers= members at 17:25:46.7; DeployLocally + the MSI leave 10/10 top-level drop files hash-identical both machines (WebContent.dll 6B281447...07FD stamp 0.1.290.10692, and the helper 39F244EC...D168 matches too - it was MSI-REGISTERED under the 290/291 chain so RemoveFiles/InstallFiles cycled it: the #89 stray gap stays scoped to unregistered Copy-Item strays); one TestUI run (McCaExport|McCaUntrustedAggregate|ScProxyTrust) + one re-run: MC-20 row 1 healthy 20261103-173817, SC-10 row1 no-banner 20261103-173951, row2 deployed-helper (hash = this drop) full refusal matrix exit codes 1/1/3/2/2/2/4 zero dialogs 20261103-174159, row3 elevated-helper import->present->remove->absent ON the ES 20261103-174158, MC-15 20261103-180100 byte-identical DER export - MC-15 was red TWICE first, harness-side only (both logs carry the ES answer + default-named Save dialog): the shell file-list System.ItemNameDisplay edit echoes the default name and the old value-first pick could take it, its UIA provider dies mid-read (0x80040201); the probe now prefers the Win32 dialog's own aid-1001 File name edit (the one the commit parser reads) with defensive ReadEditText; matrix: new IN-5 section (three rows + drift disclosure + machine state), MC-15/MC-20/SC-10 re-pass records, epic integration summary row, #72/#80/#75 trust-chain regression row, How-to-run now names the #77 compiled guard [unsigned: unattended gpg-agent cache expired, GUI pinentry unreachable]
…mes the settled vocabulary (proxied boundary = (Windows user, XProtect identity, VMS server); listener ownership; joined session) and ADR-0004 records the #87 triage decision: keep ONE shared proxied profile per browser id (WebView2 shares the UDF's process collection across host processes when options match - the #87 blank view is the self-inflicted ephemeral-port argument mismatch, not a platform wall), key the proxied profile AND the loopback-listener claim on the full proxied boundary (server leg explicit: today it enters the key only accidentally), keep the random port but publish an atomic stale-detectable claim (browser id, port, owner pid, fingerprint) so joiners compose identical proxy args, accept within-boundary session sharing in #69 diagnostics, defer owner-exit handoff semantics to the #90 lifetime probe with #87's in-view ownership error as the interim contract; rejected per-process-UDF+cookie-replay, bare deterministic port, machine-level helper; cross-refs ADR-0001/2/3 and flags the non-proxied username-fallback cross-server keying gap as out of scope
… two-role WPF host (spike/WebView2LifetimeProbe, never referenced by the plugin, built with UxTests via ReferenceOutputAssembly=false) journaling INIT_OK/TICK/PROCESS_FAILED/TICK_FAIL/SECOND_INIT_* per role against ONE shared UDF with identical options (--disable-renderer-backgrounding shared arg: the very argument whose per-process mismatch IS #87); four UxTests rows (filter FullyQualifiedName~ProbeLifetime, no VMS needed): joiner-attaches (INIT_OK + dual-tick + WMI witness that NO msedgewebview2 process names the joiner as parent host - attach, not spawn), creator-clean-exit, creator-hard-kill (host-only Kill - Chromium children left to WebView2's own lifetime logic), joiner-clean-exit control (creator keeps ticking, zero ProcessFailed); measured answer for #91: the browser process is tied to the attached-hosts SET, not the creator - it survives BOTH clean exit and hard kill of the creating host while a joiner remains, and when the collection IS gone (tree-kill artifact) a fresh EnsureCoreWebView2Async in the survivor respawns it (SECOND_INIT_OK ~13 s); harness pitfalls recorded: tree-kill kills the collection directly (artifact, not platform behavior), WPF needs [STAThread] Main, second-control parenting needs Content detach first
…wser process survives the owner host's clean exit and crash while a joiner remains, so handoff = stale-claim re-claim + epoch bump + forced recycle of the boundary's proxied views (fresh init respawns a destroyed collection, ~13 s measured); status line updated accordingly
…ry (ADR-0004 child A) - the VMS server leg becomes explicit: BrowserProfileKey gains DeriveServerKey (sanitized lower-case host + port fold) + ComposeProxiedProfilePath (pure, both Compile lists) composing <root>[\\<user key>]\\server-<server key>\\proxied-<guid D>; BrowserProfileIdentity gains GetCurrentServerKey (MasterSite.ServerId.Uri, the repo's own server idiom) + GetProxiedUserDataFolder (placeholder server-unknown when the leg is unresolvable, logged; never matches a resolved server); BrowserManager's single proxied-folder choke point re-routed + both proxied log lines now name the boundary profile path; non-proxied keying untouched per ADR-0004; no migration - old-shape folders become #94 orphans. Same tuple same path, any differing leg a different path: same identity on two servers can never share a proxied profile or fetch channel. 15 BrowserProfileBoundaryTests decision rows (same-identity different-server separation, three identity fallback legs, missing-user root fallback STILL server-fenced, placeholder rows, hostile ..\\..\\..\\evil legs staying three sanitized segments, server- prefix never a traversal token); SC-11 live probe ScProxiedBoundaryProbeTests PASSED 20261103-201830: the live proxied browser's OWN --user-data-dir names server-vms.joshooaj.com-443, zero old-shape writes (two earlier reds harness-side: the real --proxy-server=http=http:// shape, and the runtime appending \\EBWebView to the process command line - pitfall recorded: claim files/sweeps sit under the plugin-requested UDF, never inside EBWebView). 1170 unit tests + Lint green
…ram.cs with a blank-line-using WHITESPACE error the multi-command Lint Exec block masked (only the last native exit code propagates); re-green
…ure, unit-tested logic (NOT yet called from BrowserManager - that is #95's join wiring) - ListenerClaim.cs: versioned line schema (webcontent-listener-claim/1 + browserId/port/ownerPid/configFingerprint/claimedAtUnixMs), strict parse that never crashes on hostile content: foreign version or corrupt = Untrusted-with-reason, unknown FUTURE fields inside a known version tolerated (versioning contract), a claim naming a different browser id is never trusted; ListenerClaimCore.Decide = the whole what-to-do table over injected liveness seams (no-claim => ClaimNew, live => Join, stale-by-dead-pid / stale-by-refusing-port / corrupt / foreign => ClaimOverStale with audit reason, own different-port claim => replace, JoinFingerprintMatches reported); ListenerClaimStore.cs: the one thin IO seam (IListenerClaimFiles) + production probes (GetProcessById; 250 ms loopback TcpClient connect) + the bounded protocol loop whose atomic create is File.WriteAllText(temp)+File.Move (a bare CreateNew leaves an empty-file window a joiner would parse as corrupt and DELETE the fresh owner's claim - rename makes the slot transition empty->complete atomically, and the rename throwing on an existing destination IS the loser condition; .tmp litter is #94-sweep shape); claim path derived from child A's boundary: BrowserProfileKey.ComposeListenerClaimPath = the #92 profile path + '.claim' sibling in the server-<leg> dir (never inside EBWebView per the recorded pitfall); tests land in both places per AGENTS.md (test csproj Compile links). 25 new rows: full decision table no-FS-no-process (claim-new, join-live, stale-by-pid, stale-by-port, corrupt, foreign-schema, cross-id, own-stale, fingerprint-mismatch, round-trip + tolerant-future-field rows) + real-temp-dir rows (owner write, live join leaves the slot byte-identical, dead-pid/dead-port/corrupt/foreign takeovers each audit the file path + reason, idempotent self-reclaim reports Owner) + the acceptance race: two concurrent claimers over real files resolve to exactly ONE Owner + one Joiner reading the winner's port with exactly one claim file surviving; production probe smoke rows (self pid alive / ghost pid dead / live TcpListener connectable / port 1 refused). 1195 unit tests + Lint green
…tale claims (ADR-0004 consequence) - each Smart Client startup reaps its own disk litter, off the UI thread, never fatal, the live browser's folder never forced: ProfileSweepCore.cs = the pure decision table over injected facts (folder: locked skip first whatever the age, undetermined activity NEVER staleness, untouched strictly past the 30-day retention => reap, boundary/future timestamps pinned; claim: own-pid claim never reaped, sibling-folder-gone reaps, everything else delegates staleness to the SAME #93 ListenerClaimCore.Decide seam - dead pid/refusing port/corrupt/foreign/cross-id reap with the shared audit; temp litter: older than 24 h cannot be a mid-flight create => reap; shape predicates keep every non-plugin name invisible to the walk); ProfileSweep.cs = the thin IO seam: shape-bounded walk over BOTH the #92 boundary shape and the pre-#92 old shape (and the root-level fallback), never descending into EBWebView or any non-shape dir, bounded early-exit activity scan (20000-entry cap => keep), lockfile exclusive-open probe, deletes best-effort - a refusal is journalled as the final never-force arbiter, every outcome logged exactly once; BrowserProcessLockWitness.cs = plugin-only once-per-sweep Win32_Process msedgewebview2 CommandLine witness (System.Management stays out of the test-linked seam - guard-pinned); WebContentPluginDefinition: Init schedules a one-shot 10 s threadpool timer, callback composes BOTH witnesses + journals every outcome under source ProfileSweep under an all-catching never-rethrow guard, timer stopped in Close. 35 new rows: ProfileSweepCoreTests 19 no-FS decision rows + ProfileSweepDiskTests 12 real-temp-dir rows (both folder shapes, EBWebView untouchable, root fallback, sibling-rule claim reap, own-pid keep, hostile foreign files untouched, lockfile probe held-only-while-exclusive-open-fails) + 4 wiring guards (Init schedules only, callback swallows+composes+tears down, both Compile lists, witness isolation); SC-12 live probe ScProfileSweepProbeTests PASSED 20261103-220923 on the REAL data root: 12-row decoy table resolves exactly per the decision table, journal line 11 scanned / 2 reaped / 1 skipped locked / 3 claims + 1 temp reaped, real user state (two pre-#92 orphans inside retention, the live boundary folder, both EBWebView trees) untouched; two reds disclosed, both harness-side: the held lockfile written FRESH made decoy (k) survive by the freshness rule not the lock witness (now stamped stale), and File.ReadLines sharing is refused by the running client's MIPLog.txt logger (bystanders need FileShare.ReadWrite). CONTEXT.md gains the Profile sweep domain term. 1230 unit tests + Lint green
…s replace Copy-Item strays - the helper csproj now carries the SAME Nerdbank.GitVersioning PackageReference as the plugin project (root version.json stays the single version source, no second version file), so the drop helper reports the build's nbgv FileVersion (verified live: drop helper 0.1.298.22478 == WebContent.dll same build) and MSI file-copy rules (never replace an existing file whose version equals or outranks the payload) stop letting the unstamped 1.0.0.0 helper shadow every upgrade forever. Setup installer untouched - the drop harvest picks the stamped exe up automatically. TrustHelperVersionStampingGuardTests: 3 source pins (nbgv authoring on the helper csproj + no second version.json beside it, plugin keeps its own reference so the lockstep check is not vacuous, helper/plugin nbgv versions equal). Out of scope honored: no stamping of the WebContentServiceCtl/WebContentProxyCa Binary streams (re-streamed every install), no repair logic for machines already carrying the 1.0.0.0 stray (its MAJOR outranks any 0.1.y stamp; pure-msiexec migration in the README covers it), no MSI component/copy-rule changes. 1233 unit tests + Lint green
….es-upgrade-89.ps1 mirrors the IN-3 row-1 procedure WITH the fix: pre-state (registration/CA/DACL/drop hashes+versions), Copy-Item-seed of an OLDER stamped helper (.cache\seed89, the 0.1.298.x pre-fix build's exe) into the live drop, staged+hash-verified msiexec /i /qn upgrade with default options, service journal, MIP-log + MSI-log action evidence, then a full recursive local-vs-remote drop hash+version table with an explicit mismatch count (the IN-3 original ended 12/13 with the 1.0.0.0 stray surviving; this row must end 0 mismatches)
…opy-Item stray on the live Event Server (2026-11-03 22:30): seeded the drop with the previous build's STAMPED 0.1.298.22478 helper via Copy-Item (hash re-verified on the machine), ran the 0.1.300.30776 default-option MSI upgrade (exit 1641 = the WebView2 reboot-request piece of IN-2/IN-4, machine not rebooted, files proven replaced inside the run), full stop(1026 22:30:51.770)->provision(22:30:57.226)->restart(22:30:57.525) chain inside one script, old-product REMOVE script skipped all three, POST one registration 0.1.300.30776 + CA E4D2.../container/DACL byte-identical + Proxy fetcher ready 22:31:04.9 - and the VERDICT: recursive local-vs-remote hash+version table 13 local / 13 remote / mismatches=0, the stray replaced where IN-3 row 1 ended 12/13; harness disclosure: the journal Receive-Job hit a transient WinRM I/O abort after msiexec, post-state re-ran on a fresh session, journal file not written, action-schedule rows stand in; matrix gains the summary row + a regression-table pin (de-stamping silently re-opens the gap); machine state: ES now 0.1.300.30776 single registration, past the 1.0.0.0-stray transition class
…nts an in-view profile-ownership notice instead of a blank WebView (ADR-0004 point 5 interim contract) - the collision shape: the boundary profile folder (child A/#92) is deterministic per proxied boundary while every process starts its loopback listener on an ephemeral port and bakes that port into --proxy-server, and WebView2 refuses a shared user-data folder whose options differ from the owning browser process, so the second Smart Client on one machine died inside EnsureCoreWebView2Async and sat BLANK with one generic log line. BrowserInitFailure.cs = the pure seam: TryPresent(proxied, id, message) presents nothing for a non-proxied failure (its log-only surface untouched) and for a proxied failure one Presentation - the NEW ProxyErrorPage.CategoryProfileOwned ("browser-profile-owned") cause/headline/explanation (unit-pinned NEVER mapped from any ProxyFetchErrorCodes wire code, so it reads as machine-local ownership, not a gateway fault), a detail naming the browser id + the verbatim technical message, and the ONE log line naming cause + id + message + the reload remedy. BrowserInstance: the CoreWebView2InitializationCompleted failure branch and the Initialize() catch both route through PublishInitFailure (single call site, one log line per failure, InitFailure property + PropertyChanged names InitFailure/WebView); a new init attempt or a success clears it. WebContentUserControl: the browser host Panel DockPanel->Grid so a profileOwnershipError overlay (cause/headline/text/detail Border, AutomationIds profileOwnershipCause/Headline/Text/Detail) occupies the WebView area, the dead WebView is collapsed while the notice stands, and PropertyChanged(WebView) re-attaches a recreated control to the open view. Toolbar: BrowserCommandAdmission.IsAvailable gained canRecreateProfile - an errored proxied view admits Refresh ALONE (every other action still refused; the non-proxied crash table unchanged); BrowserCommand.Execute routes that Refresh to WebContentViewModel.RecreateBrowserForRefresh -> BrowserInstance.RecreateWebView (same restore-address/isolation rules as view Init, BrowserManager.TryGetBrowser resolution), NEVER Reload on a dead control; the Refresh button carries explicit AutomationId NavReload. 17 new rows: BrowserInitFailureTests 5 (pure classifier table incl. no-wire-mapping + missing-message pins) + ProxiedInitErrorWiringGuardTests 7 (single publish point, re-creation-only Refresh, address/isolation rule, re-attach, notice elements + probe ids + WebView stepping aside, both-Compile-lists pin) + BrowserCommandAdmissionTests FailedProxiedView rows + ProxyErrorPageTests #87 wording rows; class registered in BOTH csproj Compile lists per AGENTS.md. SC-13 live probe ScProfileOwnershipProbeTests PASSED 20261103-233457 on the live VMS: two real Smart Clients onto one proxied boundary (browser id witnessed from the live msedgewebview2 --user-data-dir leaf proxied-<guid>), joiner renders cause "Browser profile owned by another session" + headline + boundary explanation + detail naming 2963db34... with the verbatim 0x8007139F refusal, owner view stays healthy/notice-free, NavReload Refresh re-attempts via the re-creation path, notice reappears, NEITHER client dies. Two environment facts measured and disclosed by the probe: (1) the machine-wide Smart Client MIPLog.txt is single-writer while a client lives - the second concurrent client journalled NOTHING (not even its plugin-load line, across runs), so the probe enforces the joiner ownership line only when the run proves the joiner logged and otherwise takes a disclosed NOTE branch with the line unit-pinned; (2) killing the owner client takes its proxied msedgewebview2 down with it (probe 20261103-232700 found no surviving holder) - no crash-orphan collision shape on this runtime. Out of scope honored: no join wiring (#95), no stale-claim re-claim/epoch handoff (#96), no server-side #69 diagnostics, non-proxied init-failure UX unchanged. 1250 unit tests + Lint green; drop FB6913E2...6F4EA DeployLocally-verified (client-side-only change, ES drop untouched)
… claimed loopback listener - the join wiring #87's interim contract deferred (ADR-0004 points 3-5). ProxyListenerLease.cs = the seam BrowserManager consumes (itself not test-linked; wiring pinned by source guards): Acquire reserves an ephemeral loopback port FIRST (TcpListener(0) bind+release), then the UNTOUCHED #93 ListenerClaimStore protocol resolves the slot - no claim => claim the reserved port (Owner lease; the owner then binds EXACTLY the port its claim names, and a SocketException on that bind releases the claim and falls back ephemeral - the claim never names an unbound port), Trusted live claim => Join carrying the owner's port verbatim, stale/corrupt/foreign takeovers + audits ride the #93 core unchanged. TRIAGE (fills the brief's gap): a live claim whose configFingerprint differs JOINS ANYWAY and journals the mismatch exactly ONCE - the boundary profile is shared regardless, a refusal would resurrect the #87 blank view, and re-claiming over a live owner would tear its listener out. Joined browser: NO listener, NO own #69 session/forwarder (its traffic counts in the owner's ES session), and it keeps the #88 view https-gate via ProxyViewTargets/ProxyViewFloorHosts copies on BrowserInstance - the gate registers without a listener and the handler resolves targets listener-or-copies; config edits re-push the copies (UpdateTargets parity with the listener path). Claim release deletes a slot ONLY when it parses Trusted AND names the releaser's own pid - wired at browser Dispose, proxy-disable, failed browser create, and StopAllProxyListeners; live-foreign/future-schema/corrupt slots are NEVER evicted by a release (#94's sweep + the takeover audits own those shapes). BrowserProfileIdentity.GetListenerClaimPath derives the slot from the SAME boundary legs as the profile folder (the .claim sibling, never inside EBWebView per the recorded pitfall); ProxyBrowserArguments.PortIndependentFingerprint = SHA-256 hex over the For(0) args with 127.0.0.1:0 folded to 127.0.0.1:{port} - one claim-file-safe token, port-independent so owner and joiner args match by construction; LoopbackProxyListener.Start(int port = 0) binds exactly the claimed port. 22 new rows: ProxyListenerLeaseTests 14 (the whole decision table over a fake claim store + port reserver - reserve-first, live-owner join holds nothing, fingerprint-mismatch join with exactly ONE audit and no reclaim, dead-pid/refusing-port takeover audits, own-earlier-claim quiet replace, unsettled protocol unclaimed fallback + giveUp audit, reservation failure never touches the slot, release keeps live-foreign + future-schema + missing slots alone, two providers racing one slot => exactly one owner + one joiner) + ListenerJoinWiringGuardTests 6 (consult-before-bind at EVERY proxied creation and the proxy-enabled-since re-wire, joined browser binds no listener and carries the owner's port verbatim, owner binds exactly the claimed port with release paired to every claim write, joined view gate + targets track config edits, claim path derived only from the profile's boundary legs, both-Compile-lists pin) + ProxyBrowserArgumentsTests 2 fingerprint rows. SC-14 live probes ScListenerJoinProbeTests, 3 rows PASSED on the live VMS (runs 20261104-011831/012050/013207, drop 2CC41544...E518 via Build, DeployLocally - client-side-only change, ES drop untouched): (1) two live Smart Clients render proxied pages on ONE DevTools endpoint (2 page targets), exactly one proxied-*.claim naming the live owner pid 12724 + port 64423, WMI shape 0 msedgewebview2 parented to the joiner / 0 --parent-pid naming it / exactly 1 owner-rooted browser / exactly ONE loopback --proxy-server port == the claimed port, no profileOwnershipHeadline on either view, the joiner's join log line conditionally enforced (the measured #87 single-writer MIPLog caveat stands - NOTE branch, process witnesses authoritative); (2) the owner's page re-navigated (NavReload) exactly mid the joiner's creation still lands - both render, neither dies; (3) the MC Status tab from a PROVEN-ZERO baseline shows exactly "1 live proxy session(s)" + one grid row with BOTH clients rendering - the naive non-shared shape fails the row; one harness red disclosed (012232): a previous probe row's killed client leaked an ES session the 3-minute idle backstop (30 s sweep) had not reclaimed, so the row now opens MC FIRST and demands baseline zero. SC-13 live probe ScProfileOwnershipProbeTests RETIRED by this ticket - its witnessed shape (second client refused with the in-view notice) is contractually dead now that the second client joins; the notice stays in the product as the browser-held fallback, unit-pinned by the #87 suite unchanged, SC-13 in the matrix keeps the historical record + supersession note. Matrix gains the SC-14 section, the summary row, and a high-risk-regression pin (collision/double-listener/claim-eviction/session-split regression -> lease + guard tests + SC-14). Out of scope honored: no epoch/handoff re-claim (#96 - its handoff rides this same lease seam + the #87 Refresh re-creation route), ListenerClaim/ListenerClaimCore/ListenerClaimStore (#93) untouched. 1272 unit tests + Lint green
…the watchdog that forces the boundary's proxied views onto the handoff owner (ADR-0004 point 4). ListenerClaim schema v2: required epoch field (generation of the boundary listener) - pre-epoch v1 parses FOREIGN per the #93 rule (stale + audit), never a crash; ListenerClaimCore.Decide takeover rows publish claim.Epoch+1 (ListenerClaimMove.NewEpoch), absent/corrupt/foreign slots use the caller floorEpoch, and the store never lets a floor REGRESS a higher on-disk epoch. ProxyListenerLease surfaces Epoch+OwnerPid; Acquire gained floorEpoch + pinPort (owner-side slot repair names the port ACTUALLY served - joiners' baked --proxy-server argument stays truthful, same generation). BrowserInstance records the claim posture (ProxyClaimEpoch/OwnerPid/Port, ProxyClaimPath presence = ownership posture, ProxyLastInitAttemptUnixMs + ProxyHandoffAttempts, IsViewLive()). ClaimWatchCore.cs = the PURE decision table (test-linked): transient read ERROR is never stale content (None - a momentary read refusal must not evict a live owner's claim); NotFound => owner Republishes / joiner Rewire; unparseable => Rewire through the protocol; slot names own pid while posture says joined => Rewire (adopt); own claim naming a different port => Republish(port actually served); foreign claim rides the #93 staleness rule FIRST (dead pid / refusing port => Rewire = the handoff trigger), then posture agreement (owned posture vs live foreign owner => step down to join; joined posture with owner-port-or-epoch drift => Rewire into the new generation); claim still describes exactly what the browser carries but the WebView never attached => Retry inside a bounded budget (MaxHandoffRetries=5 per generation, ReCreationGraceMs=15000 > the ~13 s respawn measured in #90), GiveUp ONCE then quiet - closed views (no init failure) are NEVER resurrected, cycle terminates. ProxyListenerClaimWatchdog.cs = plugin-only 10s/5s Timer, tick fully guarded; BrowserManager.WatchBoundaryClaims reads one claim per CLAIMED (epoch>=1) browser, decides via the core with the #93 store's OWN production probes, dispatches Rewire/Retry to the UI thread. PerformHandoffRewire (UI, the ONLY recreation call site in the manager): re-consults _leases.Acquire with floor = owner keeps epoch / joined bumps, UnclaimedFallback changes NOTHING, promoted owner builds the listener via StartListenerOnLeasedPort on exactly the claimed port, joined/stepped-down stops it + ReleaseOwnClaim, then arguments changed OR budgeted retry => browser.RecreateWebView (the #87 route - live browser processes cannot hot-swap process-level args, #90); RepublishOwnClaim re-publishes pinned+same-epoch through the untouched protocol. 45+ new rows: ClaimWatchCoreTests 15 (full table incl. THE dead-owner-while-joined row, error-read keep, recycle-window Retry, grace refusal, never-resurrect-closed-views, one-GiveUp termination, owner-dies-mid-handoff bounded restart) + ProxyListenerLeaseTests 3 (lease surfaces Epoch/OwnerPid, dead-owner takeover bumps epoch through the lease, pinned-port republish same generation) + ListenerClaimCoreTests/StoreTests epoch rows (v2 round-trip, v1-as-foreign, malformed epoch, on-disk floor + monotone takeover) + ClaimHandoffWiringGuardTests 6 (watchdog armed at BOTH proxied entries + stopped exactly once, Decide fed the PRODUCTION probes, UI-thread dispatch only, rewire reclaims via the seam + recreates through the ONLY #87 route, republish pins served port + keeps generation, core purity + both-Compile-lists with the timer deliberately test-excluded) + #95 guard counts re-pinned (Acquire 2->4, joined-branch 2->4, ReleaseOwnClaim 2->4, audit loop 2->4, For(lease.Port) + StartListenerOnLeasedPort 2->3, claim-path 2->3). Out of scope honored: no hot-swap of running-browser args, Event Server side untouched. 1310 unit tests + Lint green
…hen called off the UI thread - the FIRST live run of the claim watchdog (MIPLog 03:27:25+) proved the timer-thread read of CoreWebView2 throws InvalidOperationException(VerifyAccess), which killed EVERY watch tick before Decide ran (ExceptionHandler journalled it every 5 s; the probe could not heal because nothing healed). CheckAccess fast-path keeps the UI-thread callers allocation-free; the timer path Dispatcher.Invokes one cheap property read per browser per tick - the UI thread never waits on the watch, so no deadlock; ObjectDisposedException still answers false (closed view). Same live run CONFIRMED the rest of the chain end to end: a fresh client's startup consult took the dead-previous-owner claim over with the audit 'the owner process (pid 26656) is no longer running' and bound the claimed port as claim owner, and #94's sweep correctly KEPT the now-live own claim. Wiring guard gains the CheckAccess/Dispatcher.Invoke pin so the affinity fix cannot regress. 1310 unit tests green
…andoff, and the Handoff vocabulary. ScListenerHandoffProbeTests, 2 rows, both PASSED on the live VMS (runs 20261104-033614/20261104-033857, drop 2D260723...FED5 via Build, DeployLocally - client-side-only, ES drop untouched): (1) clean-close row - owner claim pid+port+epoch N witnessed, second client joins (2 pages on the shared browser process), owner MAIN WINDOW CLOSES (clean exit, process exit proven), then within 180 s WITHOUT user action the claim ends exactly at v2 naming the SURVIVOR pid, a different port, epoch N+1 (3->4, 56765->60383), the survivor's proxied page RE-RENDERS on CDP, WMI shows exactly one loopback --proxy-server port == the new claim port, and the watcher's own notice + 'Boundary claim handoff re-creation ... (attempt 1 of 5, epoch 4)' lines are ENFORCED not NOTE'd (the survivor journals once the other client is gone - refines the #87 single-writer caveat); closing the LAST view then leaves ZERO claim files (no dead-owner slot lingering for hours); (2) hard-kill row - Process.Kill() of exactly the owner (NOT KillClients: it would take the survivor too), the stale dead-pid claim carries the handoff: epoch 1->2, port 55582->52424, page re-rendered, one proxy port; the bounded retry proved itself live (attempt 2 of 5 landed after attempt 1 met the still-recycling browser process). Harness disclosures: the first probe sessions died at Maximize because the watchdog's IsViewLive hit WPF VerifyAccess from the timer thread (fixed + pinned in 451c82c - the failed sessions' MIPLog usefully proved the takeover chain live: fresh client audited 'the owner process (pid 26656) is no longer running' and bound the claimed port); the probe hardens Maximize against the window-transition race. Matrix: SC-15 section (contract bullets, automated+live witnesses, full live record incl. the two disclosures), summary row, high-risk-regression pin (stuck-dead-port / Error-read eviction / double-take-without-epoch-bump / hot-swap / endless re-creation / resurrect-closed / lingering claim -> watch core + epoch store/lease rows + handoff guards + SC-15). CONTEXT.md gains the Handoff term (the brief's demand: the surviving process's claim take + view re-creation; avoids failover/takeover-alone). Lint green
…e pwsh-7 ternary - the CI Build step runs shell: powershell (Windows PowerShell 5.1) where '? :' is a parse error that killed the whole script before any statement ran; matches the if/else style of the sibling RestartEventServerService row; parses clean under the 5.1 parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.