mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-14 19:16:40 +00:00
* docs: add the sub-phase 2.0 probe corpus plan The plan for the Windows platform-exploration sub-phase: an eleven-area probe corpus, its findings ledger, and the same-PR phases.md corrections that ledger obliges. Lands on the 2.0 sub-phase branch so the sub-phase PR reviews as one self-contained unit. * feat: add windows probe harness with redaction gate and integrity helpers The harness is the foundation every 2.0 probe codes against: common.ps1 owns the R11 redaction gate, BOM-less UTF-8 capture writers, KTD9 normalization, scratch-process lifecycle with pid tracking, the KTD5 foreground assertion, and a Medium-integrity process launcher. runas.exe /trustlevel:0x20000 was measured on this box and produces a High integrity process (S-1-16-12288) with a deny-only Administrators group, not the Medium label the UIPI probe needs. Start-MediumIntegrityProcess therefore duplicates the token and sets S-1-16-8192 explicitly, asserting the label on read-back so a UIPI row can never be graded against a fake boundary. * feat: add winforms and wpf scratch fixtures with explicit automation ids The fixtures are the interaction targets for the pattern census, identity stability diff, raw interaction exercises, and the COM shim's event sources. Two measured platform facts shaped them. First, the .NET Core rule that a WinForms control's Name becomes its UIA AutomationId does not hold on .NET Framework 4.8 -- the HWND provider supplies raw window handles instead, which change every launch and are useless for an identity-stability diff. The fixture installs a server-side provider per control, with a switch to fall back to explicit Win32 control ids. Second, Win32 UIA client-side proxies are inert on this VM: the WinForms fixture exposes 23 controls with zero patterns, every one a Pane, matching stock Notepad's Edit reporting as a bare Pane. The WPF fixture exposes 25 controls with a full pattern surface. Interaction probes therefore target WPF and record the WinForms absence as evidence rather than working around it. * feat: capture windows environment, session, and integrity baseline Every ledger row inherits these machine facts, so they are captured reproducibly rather than asserted: build 17763.7434, session and interactivity, mandatory integrity label, UAC policy state, default file ownership, display topology, and tool inventory. Identity is recorded as a shape, not a value. The account SID keeps its well-known RID tail (S-1-5-21-<redacted>-500) because the RID is the load-bearing fact, while integrity and well-known group SIDs stay verbatim for the same reason. Three findings the later units depend on. The GAC UIAutomationClient assembly exports 57 types, all managed System.Windows.Automation, and no IUIAutomation -- while CUIAutomation8 is registered against uiautomationcore.dll, so UIA3 is reachable only through a compiled interop shim. Newly created files are owned by S-1-5-32-544 via TokenOwner rather than TokenUser, pre-answering half of the private-file ownership question. The locale is split: UI culture es-ES while the current culture is en-US, so OS strings are Spanish but .NET number and date formatting is not. * feat: probe windows private-file primitives for the 2.1 hardening design Answers the four questions sub-phase 2.1 rebuilds its private-file layer against, measured on the real OS with verbatim Win32 error codes. Three of the four contradict the framing this work was planned under. Rename: MoveFileEx cannot replace a target that has any open handle, and it reports ERROR_ACCESS_DENIED rather than the sharing violation the plan assumed -- FILE_SHARE_DELETE does not rescue it, because MoveFileEx issues ReplaceIfExists rather than POSIX-semantics rename. ReplaceFile is the operation that honors share-delete on the destination, but it is the one that refuses an open handle on the replacement source. The two APIs have opposite tolerances on opposite sides, so code treating error 32 as the sharing case misclassifies the most likely production failure. Ownership: new files take their owner from TokenOwner, never TokenUser, at both High and Medium integrity. Integrity level is not the variable -- the default follows admin-group membership, so an owner-only DACL check breaks for any admin account regardless of elevation. Locality: GetFileInformationByHandleEx(FileRemoteProtocolInfo) does distinguish local from remote, but signals local by failing with ERROR_INVALID_PARAMETER rather than succeeding with a local protocol value. An out-of-range info class returns that same error, so the code is ambiguous and a control call is required before reading it as a locality signal. ACL: neither leaf carries an explicit ACE. The restriction is authored at the nearest protected ancestor and merely propagated, so a leaf-only check sees the effective grants but cannot tell whether they are anchored or silently re-derivable, and cannot see FILE_DELETE_CHILD on the parent. * feat: probe uia3 com for events, caching, walkers, and pattern availability This is the stack the Rust adapter actually wraps, and it does not agree with the managed client. Notepad's edit control is a bare Pane with no patterns to System.Windows.Automation but a Document with Value, Text, Text2, Scroll and LegacyIAccessible to UIA3 COM, which names the responsible EDIT proxy in ProviderDescription. Twenty-six nodes versus three. Win32 client-side proxies are alive here; they are inert only for the managed client, so a managed census would have recorded false absences for patterns the adapter will see. The scratch fixture's earlier zero-pattern reading was a fixture artifact rather than a platform fact: a custom server-side provider suppresses both client-side proxies and WinForms' own providers. Both fixture modes are censused so the ledger states the mechanism instead of the symptom. CacheRequest measures 2.73x against the documented 3-5x, but the headline number hides the shape: building the cache makes the find phase slower, and the entire win lands in the read phase at roughly 299x. The claim is wrong in both directions. Handler teardown is safe with events in flight, costing 69ms against a backlog of 281. The hazard is elsewhere: hiding and showing a top-level window while handlers are registered poisons subsequent removal for up to 86 seconds, superlinearly in repetitions, and cheap callbacks do not avoid it. A watch implementation needs a teardown timeout rather than an assumption. Pattern ids are discovered from the OS at runtime rather than hardcoded, which caught that the annotation availability property is 30118. * feat: dump notepad, explorer, settings, and obsidian trees with real geometry Targets launch restored-but-not-activated, so bounds are real: 100/95.9/100/99.2 percent non-empty across the four dumps, with focus asserted identical before and after all seven walks. Settings resolves through ApplicationFrameWindow to its CoreWindow's SystemSettings pid, because the frame window belongs to ApplicationFrameHost and no predicate in either script keys on a display name. Minimizing a window does not shrink its tree -- the node count is identical at 3 -- but it degenerates geometry in two different shapes. Only the top-level window reports an empty rect; its descendants report real dimensions anchored at -32000, and every node still reports IsOffscreen as false. An occlusion gate testing emptiness or IsOffscreen alone would accept both as visible. Chromium activation is not a client-stack difference. The managed client takes Obsidian from 9 to 119 nodes across an 8 second settle with no flag, matching the COM shim's shape. What suppresses it is occlusion: an earlier revision that left other targets restored on top held the count at first contact for a 16 second instrumented hold across three runs. The mechanism is not isolated, so it is recorded as a probe-placement hazard rather than a product claim. The managed timing cross-check reproduces the COM stack's direction but not its magnitude, and on Notepad caching is a pessimization at 0.21x, because client-side providers run in the client process and the cache is pure overhead. * feat: census patterns and automation ids, and measure identity survival Identity survival is what sub-phase 2.5 designs its Windows RefEntry against, and the three properties behave nothing alike. AutomationId survives process restart on every stack measured, at 100 percent across WinForms, WPF, and real Win32. RuntimeId survives restart nowhere, at zero percent on all three. Path survives restart perfectly and is the first thing a content change breaks. Under content mutation the failure is worse than loss. Explorer keys list rows by row index, so after a folder changes, 29 AutomationId keys still resolve and 5 of them land on a different file. A ref keyed on AutomationId alone does not fail -- it succeeds against the wrong element. That silent-wrong-target count, not the survival percentage, is what makes stable text identity load-bearing. Measuring it needed a real refresh wait: a 4 second settle reported a wholly unchanged tree and would have recorded Explorer identity as perfectly stable, the exact inverse of what the window does once it refreshes at 20 seconds. Two stack facts. WPF automation-peer binding is a one-shot race -- a client that reads before the peer exists binds the generic HWND provider and never re-resolves, which a 30 second poll never recovers. And the managed and COM clients report different AutomationId values for the same window, so the divergence between stacks is in values as well as in visibility. The honest pattern divergence is two, not eleven: LegacyIAccessible and Text2 are the only patterns the managed stack structurally cannot name. The larger figure was an artifact of the WPF peer race and was retracted after the fix. * feat: probe uipi, dpi awareness, and chromium activation UIPI is measured across a boundary that actually exists. This box runs the built-in Administrator at High with Admin Approval Mode off, so RunAs yields High-vs-High and runas /trustlevel leaves the mandatory label untouched; the probe manufactures a real Medium token instead and refuses to record a verdict unless the worker reads back S-1-16-8192. Both arms run identical code against the same High Notepad, so the High arm is a control proving the injection works. UIA reads succeed from Medium; SendInput does not land, and the two arms return the identical success code -- only re-reading the target separates them. PostMessage is denied with error 5 while SendMessage WM_GETTEXT succeeds, so reads cross the boundary and writes do not. The 125 percent DPI measurement is not achievable here and the reason is measured rather than assumed: the VMware display carries no EDID and offers a single scale step. DisplayConfigSetDeviceInfo returns success and persists the registry value while effective DPI stays at 96, which is itself worth recording -- that API's success return is not evidence the scale changed. The non-zero delta defers to 2.4, which owns per-monitor scale. What does land for 2.1 is that PER_MONITOR_AWARE_V2 succeeds on 17763 with both arms verified at the awareness levels claimed. Chromium activation is graded, not guessed. The bundled version was read off this installation by scanning the shipped binary's user-agent string: Chromium 142, Electron 39.8.3. Both stacks reach the same settled tree with no flag, so the exposure claim holds and the plan's candidate contradiction is superseded. The flag is still not redundant: without it first contact is deterministically the pre-activation shell, and with it first contact is a race. Neither arm removes the need to settle before trusting a first snapshot. * feat: exercise interactions, input synthesis, and hit-testing raw Every interaction records pre-state, action, then an independent re-read -- never the call's return value. Toggle proves why: TogglePattern flips WPF ToggleState without raising Click, so the fixture's status sink stays silent while the element itself changed. Trusting the sink alone would have recorded a failure that did not happen. Posted keystrokes are not uniformly dead. WM_KEYDOWN posted to a Win32 edit control does register, because TranslateMessage runs in the target thread's own pump and synthesises the character regardless of how the message arrived; what the path cannot carry is modifier state. Against Chromium it does not register at all, with every call still returning success. Establishing that needed an idle control pass: the Chromium tree moved on its own between two reads, and without a quiet baseline that drift would have been filed as a keystroke landing. The astral-plane payload survives typing. SendInput forces a surrogate pair into two separate unicode events and the target reassembles it intact, read back through WM_GETTEXT so the check is independent of both the injection path and UIA. Notepad's edit control is unreachable from the managed client, and not merely unenumerated: TryGetCurrentPattern returns false for Text, Value and Scroll on the handle-resolved element, while the COM census sees the same window as a Document carrying all three. Hit-testing is the only sound visibility primitive of the three tested. The zero-size control is addressable by handle, enumerable by no walk, and returned by no point. A minimized window reports an empty rect at the top level while its descendants report real dimensions at the -32000 anchor, with IsOffscreen false throughout -- so neither emptiness nor IsOffscreen can gate occlusion, but ElementFromPoint correctly returns the occluder. * feat: add findings ledger, session evidence, and phases.md truth-sync Seventy-nine rows across all eleven evidence areas, each carrying the stack that produced it and whether it generalizes, so later sub-phases can tell an API-contract fact from an observation about this box and these app versions. Forty-three confirm the roadmap, five contradict it, twenty-six are new edges, and five defer to a named Phase 2 sub-phase. No row is unknown. Six in-place corrections land with it. The CacheRequest multiplier is replaced by its phase decomposition, because a single number is wrong in both directions. The private-file requirements now name ReplaceFile rather than MoveFileEx for a replace over an open destination, record that share-delete is necessary but never sufficient, and permit locality inference only behind a control call, since an out-of-range info class is indistinguishable from a local volume by error code. The Chromium guidance now requires a settle before concluding a tree is thin; its exposure claim was left standing because the probe confirmed it. The event-teardown mitigation now names the hazard that actually dominates removal cost. The completeness check is a script rather than a checklist, and it is negative-tested: blanking a scope, setting a verdict to unknown, or dropping a closure point each fail it loudly. It asserts the hunk index is bijective against the measured diff rather than against a number written in prose. * fix: wake a suspended settings instance instead of failing the tree dump The probe skipped its own launch when SystemSettings was already running, then waited the full window for a frame that a backgrounded UWP instance never presents, so a second harness run failed where a standalone run passed. That is the plan's recorded process-lifetime suspension risk arriving in practice. After eight seconds without a frame the probe now issues ms-settings: to wake a suspended instance, and the failure text names suspension as the cause rather than reporting only that no frame was found. * fix: close four teardown and gating defects found by review None of these changed a measurement; all four were failure paths that could report success. The COM shim killed Obsidian by process name with a raw Stop-Process inside an empty catch, so those pids never reached the ledger. A survivor there was invisible to both the probe's own teardown and the harness survivor check -- the one place in the corpus a leaked process could go entirely undetected. Obsidian pids are now registered and terminated through the confirming path. The tree dump registered its Settings pid only after frame resolution succeeded, so a resolution timeout threw first and leaked a process the probe had launched. Registration now happens as soon as the pid exists, and only when the probe launched it, since killing a pre-existing instance would be the operator's window. The modifier-release sweep injected without a foreground assertion, the only unbracketed SendInput site in the corpus. It now asserts and logs rather than aborting, because it runs during teardown. The input probe swallowed a failed teardown capture write, so the file that proves clipboard, cursor and process cleanliness could vanish while the probe still reported ok. It now fails loudly, and a surviving process downgrades the status too. Separately, a focus change is now classified rather than assumed hostile. A target the probe launched taking focus during its own startup is the target activating itself; Electron does this asynchronously when its renderer becomes ready, so under load it lands mid-dump and failed the run intermittently while passing standalone. Only a focus move to a window the probe never launched is interference. * chore: retrigger ci after an unrelated interaction-lease flake The macOS lib-test job failed on interaction_lease::tests::home_changes_do_not_change_the_physical_interaction_lock, which timed out acquiring the physical lock inside a 100 ms deadline at a contention count of 14. This branch changes no Rust, and the other 975 tests in that job passed, so the failure is runner contention rather than anything in the probe corpus. * chore: stop committing normalized capture twins Every capture was committed twice: once raw and once with pids, handles, timestamps and bounds canonicalized. The twin is produced by common.ps1 on every run and is byte-reproducible from the capture beside it -- verified across all thirty-seven of them -- so tracking both doubled the evidence diff for no information a re-runner cannot regenerate locally. The twins are still written on disk and -Compare still diffs them, so re-runnability is unchanged. |
||
|---|---|---|
| .. | ||
| brainstorms | ||
| plans | ||
| solutions | ||
| architecture.excalidraw | ||
| architecture.html | ||
| architecture.png | ||
| example.html | ||
| example.png | ||
| faq.md | ||
| json-output.md | ||
| phases.md | ||