mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-17 20:45:50 +00:00
187 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c23203586e
|
feat: windows capture and clipboard (sub-phase 2.10) (#126)
Screenshot with Legacy GDI and Modern WGC precedence and both-sides identity, typed clipboard with contention retry and ownership re-check, probe area 22 with the evidence-lane repair, and the Win32_UI_Shell exclusion gate. |
||
|
|
d4ddb5531d
|
feat: windows system lifecycle (sub-phase 2.9) (#124)
* docs: add sub-phase 2.9 system lifecycle plan Plan the Windows System Lifecycle adapter fill — launch_app, close_app, window_op, process_state, is_protected_process, press_key_for_app, and the full window-activation focus policy over 2.6's minimal focus_window — grounded in a five-dossier research pass and hardened by an adversarial multi-lens review (verified-termination loop, WaitForSingleObject-gated exit reads, fail-closed cross-integrity activation, exact-match protected list, and press_key_for_app that verifies rather than re-activates). Correct the docs/phases.md contradictions and register the deferrals the planning research surfaced: drop the phantom WindowOp::Close variant the core enum never had; extend the §2.12 split-integrity item to own the cross-integrity window-activation/focus effect; assign the wait --menu parity hole and its menu-detection primitive to §2.11; and record the two press --app divergences (no semantic accelerator path, no headless pid-targeted delivery) in §2.15's settlement list. * docs: close the 2.9 plan's open questions with named owners The three residuals left after review were all deferrals without a receiving owner, which the repo's own rule forbids. Settle the U1 leg-7 shell-binding branch at planning time instead of "reconciling in U10": whichever way it measures, 2.9 keeps CreateProcessW and Win32_UI_Shell stays out of the manifest, because the manifest surface is a supply-chain decision rather than a probe outcome and KTD4 is load-bearing for U3's verification design. Reconciling after the fact would have surfaced the KTD10 stop-condition contradiction only once the code had shipped. Launch-by-display-name/AUMID now belongs to 2.14, the sub-phase that already owns Windows shell-integration commands and would already carry the shell dependency, written into its scope and its dependency line. Define envelope parity for failures macOS cannot produce: shared failures assert equal to the macOS pair, platform-only failures assert against the envelope contract and are recorded in 2.15's divergence list, so a Windows-only pair can never be asserted as if it were parity. Turn the FFI press-surface default from a floating concern into a U7 verification step against crates/ffi/src/actions/execute.rs, so a changed FFI path corrects the stated corner rather than relaxing a test. * test: measure windows system lifecycle gaps (A21) Settle CreateProcessW/attach, exit-code, hang, window-op tolerance, activation budget, and shell-binding facts before the adapter fills the lifecycle surface. * feat: add windows process_state and protected-process gate Give close-app a wait-gated liveness classifier and a Windows-reasoned exact-name blocklist before verified termination lands. * feat: launch windows apps via CreateProcessW Honor LaunchOptions with system-dirs-only bare-name resolution, ToolHelp attach policy, and verified first-window wait. * feat: close windows apps with verified termination Report close success only after the process handle and creation-time token show the target is gone. * feat: verify windows window ops via placement re-read Execute resize/move/minimize/maximize/restore against Win32 placement truth with an 8px tolerance pinned by A21-5. * feat: harden windows window-activation focus policy Add restore-versus-raise ordering, a finite focus-steal budget, and fail-closed cross-integrity activation mapping. * feat: compose windows press-key-for-app delivery Verify foreground ownership and integrity before synthesizing through the existing keyboard primitive. * test: stabilize press-key focus-lost invert under parallel tests Arm a deterministic focus-lost gate after the verify hook so the refusal does not depend on SetForegroundWindow races. * test: pin lifecycle envelope parity and cost baseline Lock shared macOS code/disposition pairs, class-b Windows-only failures, and A20-style hot-path cost captures. * docs: dogfood windows system lifecycle layer Judge launch/close/window-op/press-app and protected-process refusal against real Notepad and Explorer targets. * docs: sync windows lifecycle docs with shipped adapter Record CreateProcessW system-dirs launch, verified close, and press-app divergences against A21 and dogfood evidence. * refactor: split window-activate test hooks under file-size cap Keep window_activate.rs under the 400-line limit by moving invert-test hooks to a sibling module. * style: rustfmt elevation modules Apply pending rustfmt import ordering left after the lifecycle fill. * fix: bind terminate and wm-close to live ownership Verify creation-time token on the open process handle before TerminateProcess, and re-check HWND owner before each WM_CLOSE. * fix: unblock the windows lib-test gate after the ownership re-bind The ownership re-bind landed after the last local clippy run, so two gate failures reached CI. Drop a redundant isize cast in the new close test: LocalFixture::handle already returns isize, so `as isize` trips -D clippy::unnecessary_cast and fails the lib-test build. Take the on-screen stage lock in the focused-filter test. It reads the foreground twice - once through the filter, once to corroborate - and this sub-phase is the first to add tests that deliberately raise their own windows, so a sibling activation test landing between those two reads made it fail for the sibling's reason. It stages nothing itself, but the lock guards screen state rather than only screen real estate. Verified by running the suite in isolation (passes) and in parallel (failed before, three consecutive clean runs after). * fix: survive the mid-listing identity race in the live census test This sub-phase's suite spawns and terminates real processes, which is exactly the condition list_windows_live refuses on: a window whose owning process changes mid-walk fails the whole inventory. The census test expect()ed the listing instead, so a concurrent lifecycle test could panic it - reproduced on CI, not on the quieter dev box. Retry the listing rather than tolerate the first refusal, so the identity assertions still run wherever the race is not permanent, and accept only a refusal that survives every attempt and only as the exact WindowNotFound the inventory exists to report. Its sibling focused-filter test already handles the same refusal this way. Invert-verified: breaking the token assertion turns the test red, so the retry did not make it vacuous. * fix: deliver close to every owned window and stop tests passing vacuously Two defects the review pass found. graceful_close aborted the WM_CLOSE fan-out on the first window that refused the post, so every window enumerated after it never received the request. R2 requires the request to reach every window the pid owns precisely because the window that owns an app's shutdown may be enumerated after one that already tore itself down - which is the expected shape once an app starts closing in response to an earlier post in that same loop. The fan-out now continues and reports a failure only when no window accepted, leaving wait_for_exit's independent exit observation as the source of truth. Extracted broadcast_close as a seam over the poster so the behaviour is testable without Win32, and post_wm_close_if_still_owned now reports whether it delivered so a deliberate skip is distinguishable from a send. Four live press_key_for_app tests gated their whole body behind a foreground-staging bool and returned when staging did not land, so on any run where activation lost the race they passed while asserting nothing - including the only test proving the method is wired through SystemOps. Each now asserts the fail-closed contract instead of returning: a target that never reached the foreground must be refused with no synthesis, which is real coverage of the documented no-pid-targeting divergence. The trait-wiring test always calls through the trait object and cross-checks the outcome against whether staging succeeded. Invert-verified both: restoring abort-on-first-failure turns the fan-out test red, and the close fan-out tests fail if a skip is counted as a send. * fix: refuse window writes to a target that stops dispatching messages window_op and focus_window issued ShowWindow/SetWindowPos/ SetForegroundWindow with no liveness check. Those calls reach the owning thread's message queue, so a window whose thread never dispatches blocks the caller inside the OS call - the same shape A14-11 recorded for ElementFromHandle, and measured here as a call that never returned. A Deadline cannot rescue it: the budget is checked before the call and the block happens inside it, and the activation retry budget is consulted between attempts rather than during one. Both paths now ask the crate's existing SMTO_ABORTIFHUNG ping first and report APP_UNRESPONSIVE, which is what R9 already required of any path that could hang. The ping runs after identity verification, not before it, so a destroyed or re-owned handle still reports the stale-identity envelope it earned rather than being mislabelled unresponsive. Bound the live title read while here. GetWindowTextW sends WM_GETTEXT for a window owned by the calling process, so a same-process non-pumping window blocked identity verification itself before any write was reached - the documented cross-process behaviour returns the caption without sending, so this only bites in-process, but the title is best-effort evidence either way and identity still rests on the owner and generation checks, which never touch the message queue. Both guards are proven against the non-pumping StalledFixture, which hangs indefinitely without them. * fix: let the wait signal alone decide that a process is gone close.rs's exit gate vetoed on an exit code of 259, which is STILL_ACTIVE - the same value GetExitCodeProcess reports for a live process. A process that legitimately exits with that code therefore read as still running for as long as any handle kept it from being reaped, which the caller's own child handle routinely does, so close_app spun to TIMEOUT and delivered_unverified for a process that had already terminated cleanly. KTD3 says the wait decides rather than the code, and process_state gates that way already; close.rs had re-derived the check and got it wrong. Also stop broadcast_close discarding a request it already delivered. Its per-iteration budget check returned not_delivered without consulting the delivered flag, so a deadline expiring after a real WM_CLOSE had been posted told the caller nothing had happened and a retry was free. It now stops the fan-out and lets wait_for_exit report the honest delivered_unverified timeout instead. Invert-verified: restoring the 259 veto turns the new regression test red. * refactor: share the window liveness probe instead of duplicating it The hang guard landed the same probe call and the same timeout constant in both window_op.rs and window_activate.rs. It is one question about a window - is its thread dispatching messages - so it is one predicate, placed beside the handle type it takes in window_enum.rs. Only the probe is shared: activation and window operations report the same condition with different envelopes, so each caller still words its own refusal. * fix: treat restore as undoing the minimize, not forcing a normal placement show_verb demanded an exact SW_SHOWNORMAL placement to confirm a restore, but a window minimized while maximized carries WPF_RESTORETOMAXIMIZED, so SW_RESTORE correctly returns it to maximized. The ordinary maximize -> minimize -> restore sequence therefore reported ACTION_FAILED with delivered_unverified for an operation Windows had performed exactly as asked, telling the caller a retry was unsafe while the window sat correctly restored. Restore now succeeds when the window is no longer minimized, which is what the verb promises; it never promised to un-maximize. The verb and its predicate moved to their own module to stay under the file-size cap. Invert-verified: demanding the exact normal placement again turns the new maximize-minimize-restore test red. * fix: stop a trailing backslash swallowing the next launch argument Backslashes are literal except immediately before a quote, so an argument ending in one escaped the closing quote quote_arg appends: the quoted region never closed and every later argument was absorbed into it. A Windows path ending in a separator is an ordinary value, and the merge was silent - the launch still reported success. The trailing run is now doubled. An embedded quote stays doubled rather than backslash-escaped. Doubling is a valid encoding for the consecutive-quote rule and is the form cmd accepts; backslash-escaping is correct only for CommandLineToArgvW consumers and breaks a cmd redirection that works today, so the narrower fix closes the corruption without trading it for a different one. Also collapse the second copy of the window-title reader. window_ops kept its own unbounded GetWindowTextW while the identity module's copy was already bounded, so list_windows could still block on a window whose thread had stopped dispatching. One reader, already guarded. Invert-verified: removing the trailing-run doubling turns the new test red. * fix: correct the activation evidence and close the review's remaining gaps The A21-6 ledger row asserted the opposite of its own committed capture. The row claimed the uncontended first attempt always lands, 5/5; the capture records the foreground transition landing on neither attempt in any of five trials, with first_attempt_success_rate 0. The false sentence had propagated into docs/phases.md and into a doc comment on the shipped retry constant, so the product cited a measurement its evidence contradicted. Row, phase document and doc comment now state what was measured. The design stands: a finite budget with fail-closed exhaustion is better supported by 0/5 than by 5/5, because an unbounded retry against a target that never foregrounds would hang instead of failing. Two lifecycle tests targeted std::process::id() and drove the real graceful close, whose fan-out posts WM_CLOSE to every window the pid owns - including every in-process fixture window belonging to tests running in parallel, which DefWindowProcW then destroys. Both now target a child-process fixture whose window set the test owns. Production honesty: an all-skips fan-out no longer claims the OS accepted a request that was never posted, and WaitForSingleObject's failure is no longer read as "alive" when the wait could not judge liveness at all. Launch closes its process handles before the first fallible step and merges environment overrides case-insensitively, so an override of Path no longer loses to an inherited PATH. Test integrity: the launch probes no longer terminate every notepad.exe on the host - a developer's open Notepad and its unsaved work were being killed by a test run. A copied notepad is not a usable probe either; it re-execs and exits before the process table is read, and which system applications survive copying differs per image. The probe is now a copy of the test binary hosting a real window, which is repo-controlled and present everywhere. Also pins the previously untested keyboard-focus timeout branch, replaces a tautological activation assertion with independently read facts, stops a parity helper claiming a cross-platform match it only restated from literals, and removes the cwd-marker read race that CI reproduced. Captures three learnings in docs/solutions: a cited measurement must match its capture, a deadline cannot interrupt a blocking OS call, and a test that acts on its own runner acts on every other test. * docs: refresh the learnings this sub-phase moved or extended The identity-fingerprint learning pointed at window_resolve.rs for focus_window and its per-write ownership re-checks, which this sub-phase moved to window_activate.rs. The solution it describes is unchanged; only the references had drifted, so they now name where the code lives. Record the falsifiability shape this review found: a setup guard that returns instead of asserting. Four live tests opened by staging a real OS activation and returning when it did not land, so a degraded desktop passed them while asserting nothing - including the only test proving the method was wired through its trait. It is the hardest shape to catch in review because the early return reads as hygiene rather than a hole. |
||
|
|
4f7bad337c
|
feat: windows input synthesis (sub-phase 2.8) (#123)
* docs: add sub-phase 2.8 input synthesis plan * docs: harden the 2.8 input synthesis plan after adversarial review * docs: give every 2.8 deferral a receiving owner in phases.md * feat: measure windows input synthesis gaps (area 20) Probe the unmeasured SendInput and token facts 2.8 depends on so coordinate, abort-drag, double-click, integrity, and cost branches are settled before the adapter InputOps land. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: implement windows mouse_event via SendInput Give bare-coordinate hover/click/wheel a real physical path with primary-vs-virtual-desktop normalization, FFI held-edge guards, and a release-safe modifier hold around multi-click. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: implement windows drag with origin release guard Port the macOS abort-state contract onto SendInput so an interrupted drag posts corrective release at the origin and reports delivered_unverified instead of claiming a clean failure. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: implement windows keyboard synthesis and blocked combos Ship chord and UTF-16 type_text primitives with release guards, macOS key-name parity, and a Windows-specific dangerous-combo list so press/type can land without stuck keys or silent session-kill shortcuts. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: detect uipi elevation before physical input Compare caller and target integrity levels so a Medium-into-High SendInput attempt fails closed as PERM_DENIED instead of reporting a silent no-op as success. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: wire windows physical execute_action legs Replace the 2.7 not-supported stubs for type, press, multi-click, and right-click with SendInput synthesis gated by focus, elevation, and headed policy. Co-authored-by: Cursor <cursoragent@cursor.com> * test: pin windows physical input envelope parity Lock AdapterError and ActionResult wire shapes for the physical legs against the macOS disposition contract, and cite the A20-6 hot-path cost baseline already captured in area 20. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: dogfood windows input synthesis and harden delivery Prove the physical path on Notepad and ScratchForms under the corpus safety envelope, and fix the dogfood-found root-window foreground gate plus SendInput absolute normalization and drag pickup timing. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: align windows input synthesis docs with shipped evidence Restate phases, concepts, skill wording, and ledger rows so 2.8 reads as what shipped, including UIPI detection closure and the type command divergence handed to 2.15. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden windows input abort guards after review Strip plan decision ids from crates so CI phase-reference scan passes, arm a click release guard around each down/up, and re-verify keyboard focus at every type_text chunk boundary. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: give the mouse input constructors and release-guard state one home button_input existed verbatim in four files and move_input in three, so the corrective batch an abort depends on could drift from the normal post. The click and drag guards also carried the same armed-and-counted state machine and the same delivery report, differing only in their recovery hint. Both now have a single home: mouse_send.rs owns the SendInput surface - the MOUSEEVENTF_* flags with their compile-time assertions against windows-sys, plus the two input constructors - and release_state.rs owns the guard state and the delivery report it produces. Each guard keeps its own Drop, because only the guard knows whether the corrective post is a button-up in place or a move back to the origin first. * fix: resolve the host window before refusing a physical click target_window_is_foreground read the leaf element's NativeWindowHandle, which UIA reports as 0 - success, not failure - for every element that is not itself a window. That is the normal shape for WPF, WinUI, UWP and Chromium content, so double-click, triple-click and right-click were refused before injection on most modern desktop UI. The handle is now resolved the way the rest of the crate resolves it, by climbing to the first ancestor that owns one. Also from the same review pass: - mouse-wheel dropped the caller's modifiers, so the documented --modifiers shift horizontal-scroll chord silently degraded to a plain vertical scroll while still reporting success. - The drag preflight reserved one pickup delay where the sequence spends two, admitting drags that then aborted mid-gesture with the button down. Both delays now come from one constant. - The skill docs carried plan decision ids and sub-phase numbers, and they are include_str!d into the binary, so agent-desktop skills served them to agents. The phase-reference scan now covers skills/ for exactly that reason and caught a second instance in SKILL.md. * fix: close the remaining input-synthesis review findings - The click sequence counted delivered events per click and let the inter-click gap fail through a bare ?, so a --count 3 that aborted late reported two events, or none at all with an unknown disposition. Accounting now spans the sequence while cleanup stays per click. Wheel chunks report the same way: a partial scroll says how much landed. - mouse_modifier opened a second SendInput keyboard path with its own fake sink, so the one-seam invariant keyboard_send documents was already false. It now posts through that seam and reuses its VK map. - type_text's focus gate and per-chunk re-verify were exercised through a cfg(test) clone that omitted the policy gate, the preflight and the real verifier, so deleting either guard left the suite green. The gate is one production function now, with the verifier injected, and a test drives it with a verifier that fails mid-sequence. - Two drag tests asserted a std-library identity and re-typed the production step arithmetic. They drive synthesize_drag and read the posted event stream instead. Inverting the first showed the zero-dwell early return was dead - the loop guard already covered it - so it is gone. - elevation's module doc blamed unavailable token manufacture for the unproven cross-boundary effect; the captures record manufacture working with no higher-integrity target to aim at. keyboard's docs claimed its primitives were unreachable from execute_action, which calls both. * docs: capture the input-synthesis learnings and correct what the run overstated Two new solutions docs and one extended: - An enforcement gate must cover everything the binary embeds. The phase-reference scan read crates/ and src/ for .rs files while the skill markdown it never looked at was include_str!d into the binary, so plan ids were being served to agents by the skills command. Widening the scope immediately found a ninth instance nobody had reported. - A zero success value is not the answer you asked for. UIA returns NativeWindowHandle 0 as a success for any element that is not a window, and the crate already had the climb that treats it as keep-looking; the physical-click gate re-derived a naive version and refused delivery on WPF, WinUI and Chromium. - The cannot-fail taxonomy gains the cfg(test) re-implementation shape, and a prevention note to invert one production site per run: two mutations at once made a test fail on the other one's arithmetic and hid the failure that mattered. The dogfood report claimed the multi-click path more broadly than it measured it. Both multi-click judgements ran against HWND-bearing targets, which is precisely where the defect was invisible, so J4 now states its scope and the residual records that a non-HWND judgement is still owed. * docs: judge multi-click on a non-HWND target and close the residual The original run judged multi-click only against WinForms and Notepad, both of which own a window handle - the one shape where the foreground gate's leaf-handle read could not fail. J10 runs the same WPF button with the pre-fix and fixed binaries back to back: the first reports ACTION_FAILED and not_delivered with the app's click counter untouched, the second lands and the counter advances to action:2, read back independently of the envelope. Two correct refusals fell out of the same run and are recorded with it: the occlusion gate naming the terminal window that actually covered the fixture, and the private-file guard rejecting a store an elevated run had left owned by the Administrators group rather than the user. * fix: block modifier supersets of a dangerous shortcut Exact canonical-string matching let alt+shift+tab through to SendInput. That is the reverse task switcher and takes the foreground exactly as the alt+tab it extends, which is the harm the list exists to prevent. A test asserted the old behaviour as correct, reasoning that an extra modifier makes a shortcut unrelated; shift does not neutralize alt+f4 and does not neutralize alt+tab, so the premise was wrong and the test is gone. Matching is now key equality plus modifier superset. It over-blocks a few combinations nobody presses, and that trade is deliberate: a wrongly blocked combo costs one --force, a wrongly allowed one moves input to another window mid-run. macOS answers the same problem by enumerating variants (it lists cmd+q and cmd+shift+q separately), so the two adapters now decide danger by different rules. Recorded as a settlement item for the hardening gate rather than changed here, since the macOS crate is the GA line. --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5ed56337ac
|
feat: windows semantic action tier (sub-phase 2.7) (#122)
* docs: add sub-phase 2.7 semantic action tier plan * feat: measure windows semantic action write surface (A19) Probe area 19 establishes COM-stack viability for the 2.7 dispatch tier — pattern invoke, failure taxonomy, secure writes, SetFocus foreground, Legacy click, combobox/ladder geometry, and cost — so later units implement against measured branches rather than inference. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add windows mutation classifier and retrofit ScrollIntoView Write failures classify through one UiaFailure table with pinned code and disposition arms; ScrollIntoView keeps observation-owned delivery while overlays classifier code and platform detail. Scan governance covers every mutation file before new pattern call sites land. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: wire windows execute_action with click chain and honest arms Override ActionOps::execute_action so Click runs Invoke then Legacy DoDefaultAction (A19-6), SetFocus is headed-gated (A19-5), and every other Action variant returns a deliberate outcome instead of the trait default. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add windows value writes with secure-field withholding SetValue and Clear dispatch through Value and RangeValue chains; IsPassword tri-state gates every value re-read so secrets are never echoed and secure writes report verified none with delivered_unverified honesty. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add windows toggle and disclosure semantic actions Headless Toggle/Check/Uncheck and Expand/Collapse run through pattern chains with observed-state verification, tri-state double-toggle (A19-2), and LeafNode fall-through instead of blind Invoke. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add windows select and scroll semantic actions Select resolves names through SelectionItem self-match or bounded descendant search with expand-first combobox handling; Scroll drives ScrollPattern with per-axis honesty and percent or bounds verification. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add windows ancestor scroll ladder for ScrollTo Non-ScrollItem targets scroll into view through ScrollPattern ancestors with geometry-directed increments and the 2.6 visibility predicate; unsupported terminals remain only when no scrollable ancestor exists. Co-authored-by: Cursor <cursoragent@cursor.com> * test: pin windows action envelope parity and cost baseline Serialize ActionResult and classifier dispositions against the macOS wire shape in CI, and keep the A19-8 min/median/max cost captures as the tier's first hot-path baseline. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: dogfood windows semantic action tier against live targets Drive the release binary through Notepad, Explorer, scratch fixtures, and Obsidian; judge by quoted envelopes so click no longer names execute_action and residuals land with owning scopes. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: align phases and skills with shipped windows semantic actions Correct execute_action naming, Legacy click and SetFocus policy, mutation classifier and secure-field vocabulary, and the platform actions folder map so the product docs match A19 and the 2.7 dogfood. Co-authored-by: Cursor <cursoragent@cursor.com> * style: rustfmt windows semantic action modules Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: share windows action write helpers across modules Lift classify_write, verification window capping, and scroll/visibility predicates into shared helpers so per-action modules stay thin without changing delivery semantics. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: honor post-delivery disposition and ladder absence honestly Map toggle/disclosure observation failures after a real write through after_delivery, refuse to count ScrollPattern absence as ladder delivery, and split oversized dispatch tests under the 400-line cap. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: export ladder absence pin for unit coverage Co-authored-by: Cursor <cursoragent@cursor.com> * style: drop non-doc comments from envelope parity tests Co-authored-by: Cursor <cursoragent@cursor.com> * fix: stop silent wrong delivery in select, scroll, and toggle Duplicate SelectionItem names now return AMBIGUOUS_TARGET, ancestor scroll refuses invented Down directions, Check/Uncheck no longer Invoke-falls-through after an unverified Toggle delivery, and the UIPI probe uses New-NotMeasuredResult so CI cannot silent-pass an unmeasured leg. * fix: realize before select so virtualized duplicates stay ambiguous A first-pass SelectionItem hit no longer skips scroll-to-realize or short-circuits the second search, so a below-fold duplicate can still surface as AMBIGUOUS_TARGET before Select fires. * fix: keep select ambiguity and scroll disposition honest across steps Retain the first SelectionItem match across realize/re-search so a duplicate revealed only after scrolling is AMBIGUOUS_TARGET, and stamp later ladder scroll failures delivered_unverified once the viewport moved. * fix: search select matches after every realize scroll window Virtualized lists can expose same-name items only between scroll steps; searching solely at the end misses intermediate duplicates and keeps a silent first hit. --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
85f3b6e76a
|
feat: windows actionability and occlusion (sub-phase 2.6) (#121)
* docs: plan the windows actionability and occlusion work * docs: close the critique round on the actionability plan * test: measure windows actionability unknowns for hit-test and scroll A18 probes pin ScrollIntoView observation arms, unclipped straddling geometry, hang-bounded ElementFromPoint, virtual-screen Unknown guards, and WPF zero-size envelope staging before product code relies on them. * feat: add windows hit_test with never-false-negative classification ElementFromPoint classification reaches ReachesTarget or Unknown under the closed Err set, with virtual-screen and minimized guards so phantom desktop hits cannot become false interceptions before corroboration. * feat: corroborate windows hit_test interception with window attribution InterceptedBy fires only on the agreed same-root or cross-window arms, with redaction-safe occluder evidence, so false negatives stay Unknown. * feat: add windows scroll_into_view judged by observation Gates ScrollItemAvailable and verifies delivery from re-reads so the auto-scroll retry reports an honest unsupported outcome instead of PLATFORM_NOT_SUPPORTED. * test: pin windows actionability envelopes for disabled zero-bounds occlusion Fake-driven checks keep macOS-parity codes and dispositions in CI, with live disabled evidence and an opt-in WPF zero-bounds leg. * test: dogfood windows actionability against foreign occluders Exercises headed occlusion and scroll seams on real targets, unblocks window resolve for the headed path, and records envelope-backed judgements. * docs: align phases and concepts with windows actionability evidence Restates the hit-test and scroll contract from A18/U6 measurements and adds the shared actionability vocabulary the adapter now implements. * docs: align phases and concepts with windows actionability evidence Restates the hit-test and scroll contract from A18/U6 measurements and adds the shared actionability vocabulary the adapter now implements. * style: format windows actionability modules and harden live hit skips Keeps rustfmt clean and soft-skips on-screen reachability when a foreign desktop occluder covers the fixture under Cursor. * fix: require viewport evidence for scroll verification Missing viewport no longer counts as verified visibility, and the UIA client is acquired before ScrollIntoView so post-invoke errors stay observation-judged. * fix: revalidate window ownership at the focus point of use * style: fold probe commentary into doc comments * docs: state the file-size cap's enforcement scope * fix: qualify headed focus success by live window ownership * fix: bound headed focus and window resolution by the operation deadline * docs: record the window-focus surface and hand its policy depth to 2.9 * docs: tell agents what the windows actionability failures mean * test: pin that focused only ever means owned foreground * refactor: split window resolution tests into a sibling module * test: execute the real scroll gate and ban the whole read table * fix: make dogfood judgements reject the defects they hunt * test: scan the occlusion gate for banned calls and name rendering * test: make the windows lane stage the live wpf evidence it claims * fix: budget the occlusion gate's walks and scope viewport demotion to same-root * docs: state the measured deadline overshoot for the occlusion gate * refactor: give the duplicated geometry and walker helpers one home * fix: stop a discarded viewport walk from unmaking a settled verdict * fix: make the live occlusion legs immune to desktop churn * docs: give off-screen fixture parking a measurement owner * test: dogfood the hardened occlusion gate and record the judgement rebuild * docs: fold the actionability lessons into the solutions corpus * fix: shrink the headed focus write surface to the foreground call * fix: guard headed focus with full process identity at every point of use |
||
|
|
adf2c36bde
|
feat: windows resolution and live locator (sub-phase 2.5) (#120)
* docs: plan the windows resolution and live locator work
Eight units grounded in the probe corpus, the shipped 2.4 resolver, the
macOS reference implementation, and core's live-locator contracts. The
research reframed the work honestly: 2.4 already shipped a binary
exact-evidence resolver, so 2.5 upgrades it to graded three-state matching
with the fingerprint fallback Electron's measured 0% AutomationId coverage
demands, implements resolve_locator_anchor so find stops failing, and
builds the five live readers over the machinery earlier sub-phases
hardened.
A five-persona review ran before commit and its findings are folded in:
the KTD3 composition rule (core's identity_match alone would undo the
A7-3 wrong-target defense - two core verdicts compose, with
stable_text_match as the sanctioned visibility promotion), mid-search
element-vanish routed incomplete-retryable rather than terminally stale,
the reader-path liveness corroboration given a real data path through the
element wrapper, the Electron path-survival measurement added before the
tier that relies on it, the path fast-path eligibility gate and the
positive-area geometry gate, shape-only ambiguity details kept against the
macOS pattern that would violate the redaction gate, and the secure-field
question split read-side/action-side with the action remainder routed to
its owner.
* docs: close the second review round on the resolution plan
The round hunted second-order defects in the first round's fixes and found
real ones. The retryability stamp is one write, not two - a details bool
that IS the typed channel - so the completed-search stale ref now mirrors
the macOS complete-absence shape instead of a blanket non-retryable that
would have disabled core's hydration retry and failed find permanently on
first churn. The anchor variant settles path misses immediately rather
than replaying a stale path inside the adapter loop, so core's fresh
re-observation - the actual cure - keeps the deadline. The KTD3
composition gains the no-text-identity gate that kept icon-only refs from
spinning retryable, keyed on the live field rather than the verdict. The
degenerate-hash ref class is named unresolvable-by-design with the census
attributing it, the handle-payload change names its downcast consumers,
the KTD10 write-back joins U8's enumerated list, the Electron probe leg
gets its repo-controlled vault and unmeasurable branch, and four
edit-introduced contradictions from round one are reconciled.
* docs: align the resolution plan's done criteria with the second review round's decisions
* feat: measure the windows resolution unknowns (probes 17-resolution)
Stand up the A17 probe family for sub-phase 2.5 so no resolution or live-read
fact ships unmeasured: find_all versus the shipped walk on both fixtures, the
live 0/1/N candidate counts over duplicate-evidence pairs, the single-element
strict-resolve and shared-read timing envelopes, and the ambiguity census over
the WPF and Win32 trees. The WinForms fixture gains an equal-count ListBox
swap and a duplicate-evidence button pair; the swap arm triggers the mutation
by WM_APP because the custom AutomationId provider suppresses Invoke. The
Electron path/geometry leg stages a repo-controlled Obsidian vault; the first-
contact shell and an unstageable marker tree are recorded per row with U7's
judged rate as the measurement of record. A17 rows added to the ledger with
the pre-committed branches taken, the hunk index re-synced to the measured
diff, and the probe wired into the capability-probe workflow for the second
environment.
* feat: make windows resolution matching three-state
Replace the boolean candidate comparator with core's composed verdicts so a
candidate that could not be read is incomplete, never a non-match. The role
list and stable-text rules live in core only; the intended visibility
promotion exposes stable_text_match and the Windows resolver composes
identity_match for the id tier with stable_text_match corroboration only when
the stored ref carries a stable text identity (KTD3). The search gates on role,
flags incomplete on an unreadable candidate or role, and the resolver returns
an incomplete-and-retryable AppUnresponsive instead of a settled STALE_REF
when everything readable found nothing. A7-3's wrong-target pin is carried
through the composition, and the blank-cannot-refute and vulnerable
mutable-value name edges are pinned both ways.
* feat: add the graded resolution fallback to the windows resolver
The identifier-free Electron shape now resolves through two graded tiers
before the broad search: a window-rooted path fast-path (path is a locator,
never identity - a miss or refuted candidate falls back to the broad search
and is never settled on), and a geometry promotion that resolves an unreadable
candidate on a unique live bounds-hash match. The promotion mirrors macOS's
predicate with the A17-7-measured addition that the stored hash must come from
a positive-area rectangle - offscreen and virtualized elements collapse to
shared zero-extent bounds and never promote. The search family is split into
resolve_search (the plan's named seam) to hold the three files under the
400-line cap. Live fixture tests prove a blank secure ref resolves through
path and geometry on an unchanged tree and the secure value reaches no error.
* feat: classify settled absence and retry only the genuinely incomplete on windows
Resolution and live reads now carry a typed complete/retryable pair over a
three-way read-path classifier (the kAXErrorIllegalArgument lesson ported): a
settled absence like not-supported or invalid-argument is never retried, a
transport or timeout failure and a vanished element are incomplete and
retryable within the deadline. UIA_E_ELEMENTNOTAVAILABLE keeps the granularity
split - settled stale for a read of the resolved target, incomplete-and-
continue for a node vanishing mid-descent, window-not-found at root. The
completed-search STALE_REF mirrors resolution_complete_absence with retryability
derived from the code default, and resolve_element_strict runs a deadline loop
that retries only the stamped-incomplete, re-verifies process liveness through
resolve_window_root on every attempt, and stamps deadline_elapsed onto the last
diagnosis on expiry rather than discarding it for a bare timeout. Classifier
arm-by-arm tests, retry call-count pins, and the redaction pin land with it.
* feat: add the shared single-element live read and the five readers on windows
One read bends all five adapter readers: a resolved NativeHandle is downcast,
corroborated against the verified process identity stamped into the handle
payload (the KTD7 change - the resolver attaches pid and generation token at
every resolution return, and a dead provider's empty-successful reads become
STALE_REF rather than a fabricated absence), then read through
properties::read_live plus the walk's own read_label/walk_vocabulary/
into_locator_evidence composition. Essential evidence (role, value, states,
bounds, actions) must all be non-Unknown or the read fails retryable
AppUnresponsive, never a partial bundle claiming completeness. get_live_value,
get_live_state, get_live_actions, get_live_element and get_element_bounds are
thin projections; enabled and offscreen read UIA's own flags, secure values are
withheld at the reader path, and FFI ad_get('bounds') stops hard-failing once
get_element_bounds answers. Live fixture tests pin each reader, the secure
withholding, and both directions of the dead-token rule; pure tests pin the
completeness gate.
* feat: resolve the locator anchor on windows for live find hydration
Selected-match hydration (find, wait --selector, materialized queries) calls
resolve_locator_anchor; it was not_supported and every hydrated find errored.
The anchor is the path-only variant of resolve_element_strict with the inverse
classification: the path is exact from the just-walked tree so there is no
broad-search fallback, and every step is descent along a stored path that
churn makes permanently wrong, so a step that lands nowhere, a role-refuted
candidate, or a vanished node settles the attempt immediately - a completed-
search STALE_REF with one attempt and the deadline intact for core's single
fresh re-observation. Only the unresponsive/transport class runs through the
bounded retry loop, which re-walks the same cheap O(depth) path. The adapter
wires the method and the settle-vs-retry split, and the live fixture tests pin
an exact anchor path resolving, a wrong child index settling stale without
resolving a neighbour, and a role-refuted landing refusing.
* docs: dogfood the windows live resolution loop against real applications
Drive the release binary's live loop against classic Notepad on a scratch
file, Explorer on a scratch directory, the WinForms and WPF fixtures and
Obsidian (Chromium/Electron), reading the JSON: snapshot, find round-trips, and
get/is live reads all answer. The WinForms fixture's stored ref re-resolves
correctly after a real WM_APP content swap (the A7-3 wrong-target defence holds
live), and Obsidian on this box presents the first-contact shell - 4 of 6
sampled shell refs go STALE_REF on a fresh client, 2 resolve - which is the
honest, judged Electron rate, while the identifier-free fallback's web-content
rate stays owned by the 2.12 shell per A17-8. Report committed with the
environment header, per-target matrix, ambiguity corroboration against U1's
census and residuals with owners.
* docs: correct phases.md and concepts for what the windows resolution work proved
Corrections in place, never annotated: the 2.5 scope's resolve_query bullet is
rewritten to the measured truth - core owns the LocatorQuery evaluator and the
adapter ships resolve_locator_anchor plus evidence completeness (KTD2); the
snapshot value slot's secure-field behaviour is split so the read side closes
here (content-free fingerprint evidence, IsPassword-gated candidate reads,
reader-path withholding) and the action side is owned by 2.6/2.7; 2.6's scope
line no longer re-budgets the enabled/offscreen ElementState fields 2.5 ships;
and the two new scope bullets record get_live_actions' lack of production
callers and that the in-web-content STALE_REF rate is measured on the 2.12
shell (A17-8). CONCEPTS.md gains Graded Resolution and Settled Absence, both
distinct from Evidence Tri-State. Shipped source comment references to plan
decision and unit ids (KTD, U, bare sub-phase numbers) are scrubbed to what is
true about the code, keeping only the ledger row citations the phase-reference
scan permits, and the FINDINGS hunk index is re-synced to the measured diff.
* style: rustfmt the resolution and live-read modules
* fix: close the code-review findings on the resolution and live-locator work
A nine-lens review with an independent plan trace and a simplification pass
found the branch red on the repo's own gates and carrying four correctness
defects. All are closed here with tests that fail when the fix is reverted.
The resolver's anchor collapsed a tri-state: a role read that failed was
indistinguishable from a role that differed, so a transient hiccup while
walking a stored path settled a permanent stale ref in the one resolver
built to survive churn. The retry loop never retried a provider's own
timeout, because that classifies Timeout while the predicate only accepted
AppUnresponsive. A window that was merely busy carried no retryable stamp,
so it read as settled and disabled every retry layer above it. And the
live read corroborated process liveness only before its property reads,
which on this build return empty success for a corpse - the reader now
re-corroborates after the reads and settles stale on a target that
vanished, instead of burning the deadline as unresponsive.
The gates themselves were the root cause of the rest. check-rust-file-size
hardcoded python3, which does not exist on a Windows developer box, so the
comment half of the gate could never run outside CI; twenty-seven comment
violations and five oversize files reached the branch behind it. The gate
now resolves its interpreter and both halves pass. The five files are split
by responsibility, the search context is bundled to fit the parameter cap,
and a UTF-8 round-trip that mangled three ledger rows is repaired.
Three scenarios the plan named but nothing exercised are now pinned: the
selector-wait poll, count-versus-materialized agreement, and the FFI bounds
path. Tracing the first disproved the plan's own framing - the selector
wait never reaches the locator anchor - and the dogfood report records that
correction alongside the dedup deliberately left unapplied.
* fix: bind a stored window ref to the process that owns its handle
A ref stored only pid + generation token and never asked whether the HWND
it carried still belonged to that process, so a recycled handle let
resolution walk another application's tree while the verified-handle stamp
re-asserted the same stored pair it had assumed. verify_stored now checks
live handle ownership through GetWindowThreadProcessId before the token,
resolve_window_root routes through that evidence instead of its own weaker
inline gate, and the window inventory propagates the verification it used
to discard. The locator anchor inherits the gate at its existing call site,
pinned by an inverted run rather than read off the call graph.
Also closes the remaining review findings on this work: a deadline expiry
mid-read now records Unknown instead of dropping unread properties, which
had been leaving the secure-field gate open; an unreadable role in the
anchor no longer collapses into a confident mismatch; and a Timeout is
retryable only when it is explicitly stamped so.
* docs: state what window identity proves and what it still cannot
The decision record claimed a recycled handle on a different process is
caught by the window gate and one reused by the same still-running process
is caught a level down by exact-evidence element resolution. The first is
true only now that ownership is verified; the second is not true at all,
because two instances of the same dialog present identical evidence, so
the sole-candidate arm resolves one of them with no geometric
corroboration. Both are corrected in place, and the residual is written
into the sub-phase that owns it rather than left in a report.
The dogfood harness is corrected the same way: its identity judgement read
role off the stored entry, which cannot distinguish a correct resolve from
a silently-resolved neighbour, and it is scoped to what the fixture can
actually show given that its ListBox exposes no items to a COM client.
* refactor: walk a resolver descent once and honour the deadline inside it
The anchor and the strict search each carried their own sibling loop and
their own path walk, so neither consulted the deadline between cross-process
calls: up to the sibling cap could run before the budget was next checked,
and one wide step could eat a whole hydration budget. The main tree walk had
had the check in the right place all along.
Both loops and both path walks collapse into one descent parameterised by
the caller's failure classifier and its expiry policy, which genuinely
differ: the search marks its list unfinished and retries, while the anchor
surfaces a timeout, because a settled miss there would tell core an element
is gone when the truth is that time ran out. Each classifier arm is now
pinned through both consumers, as is the axis wording the two resolvers
report, and the anchor draws its budget from the same helper as the search
so the bound cannot drift below the walk that issued the ref.
The sibling cap keeps reporting a truncated list as whole, now deliberately
and documented: the widest node measured anywhere is 27 children against a
bound of 10,000, the walk that issues refs enumerates under the same cap and
refuses an incomplete tree, so no ref can name an element past it, and the
unfinished flag is subtree-wide rather than per-node - marking cap-hits
unfinished would turn every correct settled miss elsewhere in the window
into a retry loop.
* test: pin the resolution gates that passed while broken
Nine gates across the resolver, the shared live read and the HRESULT
classifier had no test that failed when they were weakened. The classifier's
drift test was worse than absent: it compared the projection to the record it
is defined as, so it could not fail - proven by running a byte-identical
replica against three deliberately broken builds, including one with a
denial-class code flipped to retryable, where it passed every time.
Now pinned, each verified by breaking the exact line it guards and watching
the test fail: the live read's completeness gate, whose deletion previously
left the whole suite green; the AMBIGUOUS_TARGET branch, including that its
error stays shape-only when duplicate evidence is what produced it; the path
fast-path's fall-through on a refuted landing, which must reach the broad
search rather than settle; and every arm of the classifier, whose disposition
and retry stamp are asserted separately because Terminal and SettledAbsence
share a stamp and a flip between them is invisible to it.
The live read also stops issuing the label's cross-process calls once the
deadline is spent, which the bounded read above it already respected. The
label read is injected so a test can pin that no call is made at all; a label
that could not be read reports as failed rather than absent, since the
absence claim is one nothing measured.
Two classifier codes stay terminal with their reasoning recorded rather than
their names trusted: an enumeration's end of list cannot reach the success
catch-all, because a UI Automation error only carries an HRESULT when it is
negative; and the no-clickable-point code describes obscured or unscrolled
view state, which is what scroll-into-view exists to remedy, so settling it
would drop candidates mid-descent as a complete empty child set.
* fix: fail a probe run that did not measure anything
Four probe harnesses reported success while measuring nothing. A pass whose
binary exited non-zero wrote a placeholder capture, the run reported ok and
exited 0, and the placeholder satisfied the workflow's artifact gate - so the
guard failed by going green, which is indistinguishable from working. A pass
that never ran at all was worse: no capture, no complaint, same green.
Only a noisy failure was caught, and by accident: a binary writing to stderr
trips the terminating-error preference. A silent non-zero exit - a missing
DLL, an access violation, an explicit exit code - went straight through.
Placeholders now say they are not measurements, each harness declares the
captures a CI run must contain, and a CI-labelled run fails naming what is
missing or degraded. A dev box still records an honest placeholder and stays
green, and a genuinely negative measurement still passes everywhere, which is
the distinction the gate has to keep: recording that nothing was found is
data, recording that nothing was looked at is not. The gate lives in the
shared module rather than four times over.
The captures now survive a red run, since a failing probe's placeholders name
what did not measure and are the whole evidence trail. The artifact step no
longer errors on finding nothing, because a probe that dies before writing
would otherwise stack that on top of the real failure and read as its cause -
what asserts a run measured something is the per-probe gate, which names the
capture instead of firing only when every one in the job is absent.
* docs: give two cross-platform duplications a receiving sub-phase
Both had been carried from plan to plan since 2.3 with no owner in the
product's own scope document, which is where the next sub-phase's planner
reads and where this repo requires a deferral to land.
The resolver error payload is mirrored per adapter rather than shared: the
same details object core reads back to derive retryability, restated on both
sides. A key renamed or a value changed on one turns a retryable incomplete
into an unretried failure on one OS only, and both crates' tests stay green
because each asserts against its own constructor.
The evidence-completeness rule has the opposite alarm and is worse for it.
macOS restates core's rule clause for clause, and core's copy has no
production caller anywhere - only its own unit test - so the rule core
presents as canonical is inert. Tighten it and core stays green while the
macOS walk keeps grading by the old rule.
Both land in the hardening gate, for reasons that differ: the first needs a
core touch this sub-phase had already spent and changes the GA adapter; the
second needs no core change at all, but is a behavioural edit to the macOS
walk and so needs the macOS review, e2e and perf baseline that only that gate
runs. The second also carries a prohibition, since Windows consumes the same
requirements type and would find local reimplementation the demonstrated
pattern.
* fix: fail a probe that declares nothing to measure
The measurement gate is only as strong as its declaration. A probe that never
declared its mandatory captures - a leg added later, a refactor that dropped
the call - had an empty expectation set, so the gap check found nothing and
the run passed having asserted nothing. That is the defect the gate exists to
catch, one level up.
The check sits inside the gap computation rather than beside it, so it needs
nothing from the caller: every probe already asks that one function whether
the run measured what it promised, and a probe still carrying the old
hand-rolled tail fails on an empty declaration too. The four hand-copied
tails collapse into one shared assertion, so the reason reported can no
longer disagree with the reason that fired, and an unknown label fails the
parameter bind instead of silently skipping the gate.
Emptiness now reads as not-measured wherever it appears: an empty list, an
empty object and a blank string assert exactly as much as the absent result
that already counted as unmeasured. Values that are merely negative stay
measured - a bare list, a plain string, a zero and a false are answers, and
false is the measured negative this gate exists to protect.
* docs: let the integration gate decide rather than also implement
The hardening gate had accumulated a measurement it had to stage itself, a
core schema change, a macOS refactor bound to no platform phase, and the
release merge of the whole branch. Wanting full-branch review and having to
ship inside the release-merge commit are different requirements, and treating
them as one is what collected the rest.
The window-handle wrap rate moves to the sub-phase that builds the fixture
and registers the first interactive runner, which is the first rig that can
stage the churn to observe it. The schema addition that consumes that rate
becomes its own sub-phase, inserted after the one it depends on rather than
renumbering successors - reusing a number would leave existing references
pointing silently at different work, the way phases 1.5 and 1.6 were inserted
for the same reason. The field is required to be optional by construction,
since stored refs are serialised and a mandatory addition would turn a
corroboration fix into a fresh stale-ref failure.
The completeness-rule collapse is not a task for any platform phase but a
standing rule: a rule core owns and no production path calls is inert, and
tightening it changes nothing while the adapter that reimplemented it keeps
grading by the old one. It has a closed precedent in the accessible-name
work and an open instance now, so it moves to the cross-phase requirements
where the next platform's planner reads it.
What is left is decisions, one promotion that genuinely needs both adapters
reviewed together, verification and the merge. The two contract decisions
state the same boundary explicitly: the gate records what was decided, and a
decision to normalise ships as its own PR beforehand, because it changes
behaviour on an adapter that is already released.
* fix: carry a walk's unread regions into the resolution verdict
The path fast-path recorded whether its walk finished, and nothing read it:
the broad search declared a fresh flag beside itself, so only its own gaps
reached the verdict. A transport fault or a vanished node met while walking
the stored path therefore left no trace, and the attempt settled STALE_REF -
the element is gone, take a new snapshot - off a walk that never finished.
That path is the only tier that descends past the search's depth cap, so for
a deeply stored element it is the sole tier that can reach it at all, and its
gap is the only evidence anything went unread. One flag now spans the whole
attempt, declared before the fast path so a second one cannot be introduced
beside it.
A missing child index still settles on the first attempt, because landing
short of a stored index is a real answer rather than a failed read. What now
retries is a faulted or starved enumeration, which is bounded by the same
deadline and ends as an unresponsive app rather than a false certainty.
* fix: tell an agent what the error actually was
Three agent-facing errors said something other than what happened.
A stale ref reported "not found in current RefMap" for a ref that resolved
fine and whose live evidence simply did not match - the core constructor
takes a ref id and formats that clause around it, and both Windows sites
passed it a whole sentence, so the shipped text was also two sentences run
together. They now build the error directly, mirroring the macOS shape while
keeping the not-delivered disposition macOS omits, and both go through one
constructor rather than restating it.
A missing window root carried no disposition at all. An unstamped error reads
as retry-permitting, so a caller polling a window that was already destroyed
would spend its whole budget waiting for it. It is now stamped from the
classifier's own table at the granularity that applies: a window root that is
gone never answers the same handle again, which is settled, not merely
unavailable the way an element mid-descent is.
An ambiguous target carried no stamp either. It is complete, because the only
route to that verdict is a search that either finished or stopped where no
further candidate could disambiguate; and it is settled, because replaying
the same walk against the same stored evidence cannot answer differently.
A property read failure was the one case where the reported defect was not
real: wrapping it left the typed retryability untouched, since that is only
re-derived when the new details carry the key. The wrap now merges rather than
replaces so the classifier's pair also survives into what a caller and a trace
segment read, and the two failure families keep the separate answers they had.
* fix: catch the plan references the reference gate could not see
The gate matched decision and unit ids and phase numbers, so shipped source
citing the delivery plan in prose passed it - a doc comment saying the plan
pre-committed to a split, or naming a Verification Contract row, tells a
reader nothing they can act on and stops being true when the roadmap moves.
It now also catches the plan quoted as an authority: the possessive, the plan
paired with a prescriptive verb, a preposition leading into it, the plan
document by name, the spaced prose form of the delivery report, and plan
section vocabulary. Ordinary English is left alone - a path is a location a
reader can open rather than an authority, and a coordinate origin is not a
requirements document.
Four comments it found are rewritten to say what is true of the code: why a
prefetch split is conditional, why an ambiguous error carries shape only, why
a role and state divergence is deliberate and what it costs a caller, and why
a vocabulary walk repeats three times.
Both gates gain a self-test that runs on every invocation, sharing the rule
under test with the fixture so the fixture cannot drift from what ships.
Building them surfaced four holes: a file was measured with a newline count,
so a file whose last line was unterminated read one short and a file one over
the cap slipped through; a capitalised sub-phase escaped both the sub-phase
rule and the phase-number rule, which needs a digit; the generated-file
exemption hung on which side of a pipeline finished first; and the interpreter
probe accepted a version whose syntax error would have read as a violation.
* fix: stop a probe panicking away its own evidence
A probe that panicked wrote no capture and left its hosted fixture process
running, so the measurement gate reported a missing capture rather than the
error that caused it, and the corpus's no-surviving-process rule broke with
it. Six panicking calls are replaced by propagation or a recorded failure; a
walk that could not run now reports that it did not measure instead of taking
the process down. Driven both ways against a real provider fault.
A survival capture recorded element names verbatim. The gate every capture
round-trips checks operator identity - user, machine, profile paths, SIDs -
and has no rule for a content name, and the helper that reduces one is a
call-site reducer a Rust probe cannot reach, so nothing downstream noticed.
The names are recorded as a digest and a length, which still pairs an element
across a relaunch, and the contract is written down where the next Rust probe
will look: reduce a content name where it is recorded.
The A17 timing rows claimed more than one environment supports. The CI leg is
wired but no capture from it was ever committed, so every number rests on the
dev box alone - the third time this corpus has taken a single environment for
a settled fact. The rows now say so, separate the ordering they do establish
from the milliseconds they do not, and name what closes them.
* docs: give the mutation classifier and a mangled error an owner
The mutation-path delivery classifier was deferred to a pair of sub-phases
without either one taking it, so no scope said what to build. It lands in the
sub-phase that first invokes a UIA pattern and already owns the typed delivery
report the classifier feeds, with the outcomes and their codes named and the
rule that matters stated: the shipped read classifier calls transport failures
retryable, which for a write is how one click becomes two.
The stale-ref constructor takes a ref id and formats "not found in current
RefMap" around it. Fourteen call sites across core and the macOS adapter pass
it a sentence instead, so the message reads as two sentences run together and
blames a lookup that never happened. Windows is corrected here; the rest are
recorded against the gate that can change core and both adapters at once,
where a promotion of the same error family already opens two of the files.
The window-handle work in the observation plan still pointed at the merge
gate after the measurement and the schema change were split apart.
* fix: self-test the file-size gate without deleting anything
The gate's self-test built its fixtures on disk and removed them afterwards,
which tripped the harness contract that forbids a shell script under scripts
from deleting artifacts at all. The ban is right and the fixtures did not need
a filesystem: the cap rule reads its argument in one forward pass and never
seeks, so each fixture is now a stream. Nothing is created, so nothing has to
be cleaned up.
The interpreter probe's fixture no longer empties the search path, which was
never quite the thing under test and is awkward where a resolvable name can
carry an extension. The candidate list is injected instead, so the real
resolution loop and the real failure branch both run, and a second rejection
now has a fixture: a command that resolves but answers the version probe with
a failure, which is what an old interpreter does and what the emptied path
could never reach.
The rule stays the single owner of the cap, called by both the tree scan and
the fixture, and the fixture stays wired into every invocation. Each of the
six ways to break it was driven and observed: the cap that never fires, the
cap that rejects everything, the count that measures newlines instead of
records, the exemption dropped, and both interpreter rejections.
* docs: refresh the learnings against what the code now does
An audit of all twenty-one learnings found the advice sound everywhere - no
recommendation had to be rewritten - but nine had drifted in what they point
at, and two were wrong in ways that would mislead a reader following them.
The envelope contract's trigger list covered a success becoming an error and
not the inverse, which is what the last bump actually was: a snapshot that
exhausts its budget now succeeds and reports incompleteness in a field rather
than failing. A reader applying the doc literally would have concluded no bump
was needed. It gains that trigger and the list of surfaces a bump has to reach,
two of which were stale and are corrected here.
The gate doc justified its portability advice by saying the gates run on
several systems in CI; they run on one, and the machine most likely to author
them never executes them, which is why a pattern that worked only under one
toolchain survived review. It gains the dimension all of this week's evidence
kept pointing at: test a check's ability to run at all, because one that finds
nothing and reports success is indistinguishable from a clean tree.
The rest is currency. The private-file section is written in the present tense
about hardening that has since shipped and satisfied it, including the
technique that made one constraint tractable - prove an ambiguous sentinel by
first making a control call that must succeed. Dogfooding against real
applications is described as the standing practice it became rather than a
recurrence. The allocator doc names the one sanctioned second recursion and
draws the boundary around it. The reliability contract's verification layer is
stated per-platform now that a second platform has one.
The pointer-actions documentation gap is closed, so its doc goes; the one
observation not mirrored anywhere else - that the actionability battery probes
several points while the pointer path checks only the one it resolved - moves
to the doc that owns pointer policy.
CONCEPTS.md gains the read-outcome taxonomy, whose only defined member had
been leaning on three siblings that did not exist, and the evidence vocabulary
that separates measuring nothing from finding nothing. Entries carrying file
paths, type names, or a roadmap owner are rewritten to stand on their own.
* docs: show the snapshot envelope the shape it actually has
Four places documented the snapshot response with a data object that predates
the current envelope: they carry the right version number beside a shape from
before it. Since that version, every snapshot reports whether it finished, and
a tree too large to walk succeeds and says so in a field rather than failing
with a timeout - so an integrator following any of these examples would branch
on an error that no longer arrives.
The C header is regenerated from its source rather than edited, and the two
examples that were also missing the snapshot id gained it: a shape known to be
wrong in one field is not worth publishing to be right in another.
Left alone deliberately: the error envelopes, which the version did not
change, and the entries that assert nothing about the data object. The
traversal timeout row stays true, because a drill-down from a stored ref still
returns one - it observes a subtree whole or not at all, so it never reports a
partial result the way a full snapshot now does.
* test: measure the adapter's own withholding, and the gate that guards it
The secure-control probe read the provider directly, and the provider blanks
those slots itself - so the arm passed for the platform's reason and would
have kept passing with the product's withholding deleted. It now takes two
readings of the same control: the provider's, whose recorded numbers are
unchanged, and the adapter's own composition. The pass is gated on the
provider having published something first, because an adapter that published
nothing is otherwise indistinguishable from a provider that offered nothing.
One target publishes no readable slot at all, and that pass now says so
rather than counting itself as evidence.
The reader test gains the same treatment. A marker pin alone would not have
caught this, because the marker never reaches the slots the provider blanks;
what catches it is comparing what the platform offered against what the
reader passed on.
The measurement gate - the check that fails a run which measured nothing -
had no self-test of its own, so a regression returning "no gap" would have
let every run report success after asserting nothing. It now carries one that
runs whenever the shared module is loaded, drives the shipped functions
rather than a copy of their rules, and covers both directions: a placeholder,
a pass that never ran, an empty declaration, and the negative result that
must still pass. Building it exposed two holes in the fixture itself, one
silent - the decision was extracted so a test can reach it without ending the
run, and the alarm is now read a second time independently, because an alarm
routed through the circuit it monitors is not an alarm.
The geometry promotion arm had no test that failed when it was deleted. The
per-node decision is extracted so both halves are pinned: an unsettleable
identity promotes when a positive-area rectangle agrees, and a zero-extent
one never promotes however exactly its hash matches.
* test: make the checks fail when what they guard is broken
Five review rounds each surfaced one more assertion that would survive its
own subject being deleted, so this sweeps the class instead of the instance.
Every test in the Windows crate was read and every claim below was proven by
breaking the exact line it guards and watching what happened.
Twenty assertions survived a mutation and now catch it: a sentinel whose
disposition could be flipped, six state producers that could each be deleted,
a property that could take another's name, two retry arms, a modal check whose
only test ran the non-Windows stub, an evidence projection whose two sources
were seeded identically and so were indistinguishable, two private-file gates,
a promotion failure tested as a formatter but never as a wired path, an action
vocabulary asserted against a superset containing names the producer cannot
emit, and three redaction rules.
Two lines guarding real defects had no test at all. Deleting the resolver's
verified-process stamp disarmed the corpse check for every resolved handle,
and turning the bounds-hash tie-break into first-match-wins silently resolved
the wrong element among hash-matched candidates - the shape measured on a real
file list. Both are pinned against fixtures where the right answer is not the
first one.
One production defect: the tree dump redacted only the first user path in a
value, and a provider chain carries one per provider, so a second path was
written out verbatim. Every occurrence is consumed now.
Eleven further lines are proven unguarded and named in the report rather than
quietly left; two pins cannot be inverted because the property they assert
comes from the live system, and they say so.
* fix: stop the gates and probes reporting what they never measured
The pre-commit hook had stopped running any gate. It selected staged files
with a pipeline whose reader quits at its first match, so the writer died of
a closed pipe and, with pipefail set, finding a match became a non-zero
status - a match read as no match. Once the staged path list outgrew the pipe
buffer the hook announced there was nothing to check and exited clean. It
reads the list once now, and both directions are driven against a list far
larger than any buffer, because this failure announced itself as success.
The npm release gate searched the whole workflow for a publishing permission
that another job also holds, so deleting the grant from the job it guards left
it passing. The bash-3 gate rejected two spellings of an associative array and
silently accepted three other constructs from later shells. The comment
scanner's own tests were named so the lane that discovers tests could never
match them, so they had never run. Each now carries a fixture that shares the
shipped rule and runs on every invocation, in both directions.
Among the probes: a comparison of two joined lists always evaluated to a
truthy string, so every path-survival and bounds-equality field reported
agreement for values that differed. An anti-vacuity guard counted a null as
one element and so never fired on the one input it exists to reject. A
registry lookup used a drive that does not exist, making a published
registration answer unconditionally false. A list-swap classification was
published from runs where the mutation never landed, and a leak check planted
its secret without reading it back, so deleting the plant changed nothing it
reported. A private-file probe published two dozen rows about renaming over an
open handle without ever opening one.
The redaction gate reached three quarters of the committed captures; it now
reaches all of them. A document title that lands on a container escapes the
content rule, because a container takes its name from its contents and the
window title only tracks the active pane - one such title was published and is
withdrawn. Chrome labels stay verbatim: the rule reduces only a name echoing
something already reduced beneath it, inside a target declared to carry user
content, which the unscoped form was measured to get wrong.
Three ledger rows are corrected to what re-measurement shows.
* fix: let the pre-commit hook run on the platform this phase targets
The hook drove clippy and the library tests across the whole workspace, which
resolves through default members and so pulls in the macOS crate. That crate
cannot build anywhere else, so the hook failed outright on every Windows and
Linux machine and the documented way to commit was to skip it. Skipping it is
how a formatting break and a batch of comment-rule violations reached CI this
week: the gates that would have caught both run in the hook.
Both invocations now take the package set the host can actually build, taken
from the CI lanes rather than invented - Windows and Linux mirror their own
lanes, and macOS keeps everything it already covered so the released platform
loses nothing. A guard refuses the commit if the macOS crate ever enters a
non-macOS set, since that is the failure being fixed. Format stays workspace
wide because it builds nothing.
Everything else is untouched, including the staged-file matching whose own
fixture still fails in both directions when sabotaged.
* docs: correct what actually gates a chromium content tree
Two scope entries recorded that a web target's tree only reaches a client
that settles long enough, and that the dev box could therefore only ever see
a first-contact shell - so the hardening sub-phase was told a longer-lived
client on its own runner was the lever. Measurement disagrees.
On this dev box a restored Chromium target answers the product's own path
immediately with a complete tree and allocated refs, while the same process
minimised answers with a timeout and shows a raw walk eighteen nodes. Holding
one client against the minimised window for ninety seconds adds nothing, and
neither does the accessibility handshake the window answers: the tree is
wholly there on the first read once the window is restored, and wholly absent
while it is not. Window state decides it, not elapsed time.
The rate inside web content is still unmeasured and still owned by the
hardening sub-phase. What changes is that it starts from a true premise, and
its first obligation is to control the target's window state rather than to
attribute a thin tree to the machine.
* refactor: split the two facts one resolution flag was carrying
A single flag told the resolver both that an attempt had left some region of
the window unread and that a particular landing was uncertain. Those are not
the same claim, and for a landing at a stored index they are provably
unrelated: the child list is only ever appended to, so a truncation drops a
suffix, and a landing that resolved an index at every level had every read it
needed. Whatever went unread lies past it.
Carrying both facts in one flag meant each reader saw the one they cared
about. One reading found the flag discarded and concluded a missing element
could be declared gone off a walk that never finished. Another found the same
flag reaching a successful landing and concluded the landing was unsound.
Both were reading a value that could not answer either question on its own.
The path walk now returns its own landing, carrying whether the walk left a
region unread as a property of that walk rather than as an argument threaded
into the tier below. Accepting a landing is a separate function that has no
completeness parameter at all, so the tier cannot be made to second-guess a
positional answer with a fact about elsewhere. The verdict takes the landing
itself, so the walk's gap cannot be dropped on the way without writing a
claim that no walk ran, next to the walk that ran.
The property this protects was never actually pinned: reintroducing the
discarded-gap defect left the suite passing. Both directions are pinned now,
along with the prefix lemma the positional answer rests on, and the tier is
load-bearing rather than redundant - a control with no settleable text
identity resolves through it and cannot be resolved without it.
|
||
|
|
bbad7721fb
|
feat: windows observation read path — snapshot, inventories, chromium settle, drill-down (#119)
Lands the full Windows read path: observe_tree wired to the shipped walker with honest completeness (partial trees, liveness-verified complete claims, budgeted boundary counts), HWND-first window identity corroborated by a process-generation token, list-windows/list-apps/list-displays inventories, focused-window composition, the four P2-O8 descriptor fields as absent-by-default cross-platform schema, Chromium detection with a deadline-bounded activation settle (--timeout-ms, --force-electron-a11y), the provenance-gated web-wrapper depth-skip, and fail-closed drill-down resolution. Verified by a nine-reviewer code review (twelve validated findings fixed with revert-sensitive tests, two rejected with evidence), a two-wave post-fix dogfood against real applications including cold-start Chromium, and a two-environment cost corroboration (A16 ledger). |
||
|
|
a2712f18a5 |
chore: merge main into the windows adapter branch
Brings 0.7.0 and the observation and action fixes (#117) onto the Windows integration branch so the promotion PR is no longer conflicting. One conflict, in CONCEPTS.md, and it was additive on both sides: main added Partial Observation, this branch added the Vocabulary section. Both are kept — Partial Observation belongs to Desktop Observation, beside Drill-down, which is where it now sits, and Vocabulary follows it. |
||
|
|
015307e7b9
|
chore(main): release 0.7.0 (#118)
Some checks failed
CI / Format (push) Has been cancelled
CI / Rust 1.89 MSRV (push) Has been cancelled
CI / Native check (macOS) (push) Has been cancelled
CI / Native check (Linux) (push) Has been cancelled
CI / Native check (Windows) (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / FFI Python Smoke (push) Has been cancelled
CI / FFI Header Drift (push) Has been cancelled
CI / FFI Panic Guard (push) Has been cancelled
CI / FFI Stub-Adapter Passthrough (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / FFI Release Gates (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
* chore(main): release 0.7.0 * chore: sync cargo lock for release 1.89 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
32175e44c5
|
fix: return observed trees and stop demanding renderer activation from shallow walks (#117)
* fix: return observed trees, stop demanding activation from shallow walks Six defects found by measuring the observation and action paths against real applications. Each was code computing something correct and then discarding it. Snapshot discarded a fully-observed tree when its budget expired. Finder observed 247 nodes, threw away all of them, and returned TIMEOUT with zero refs. It now returns what it observed with `complete: false`, and every node whose descendants were cut carries `subtree_truncated`, which propagates to its ancestors so a reader can walk from the root to the boundary. Only a full snapshot may be partial: a drill-down replaces refs inside an existing map, so it still requires a complete observation rather than destroying descendants it may not be able to re-allocate. `kAXErrorIllegalArgument` was classified as retryable. It is the window bridge rejecting an element outright, which no amount of retrying will change, so strict resolution burned its full budget on a call that could not succeed and then blamed the application. Finder refs went from 0 of 9 resolving at ~814ms to 6 of 7 at ~120ms. A depth-clamped observation was treated as evidence that a renderer had no accessibility surface. A shallow walk stops above the web content by design, so every skeleton snapshot of a Chromium application demanded an activation it did not need and re-walked the tree until the deadline expired. Slack `--skeleton` went from failing at 3.4s to 0.2s, and depth now scales monotonically instead of shallower being slower than deeper. The retry that follows a genuine activation also backs off, because each attempt costs a full tree walk: a 3s budget spent about 120 of them and now spends 11. A boundary node is read for its child count alone. On a renderer that materialises children lazily that count can cost more than the traversal it describes, so it is now best-effort; a boundary that cannot afford one is still reported as truncated, just without a number. Refmap retention kept 512 snapshots and swept every one of them for orphaned temporary files on every save. Retention is now 128 with eviction to 96 so the sort-and-stat pass is amortised, the per-save sweep covers only directories that save could have written, and the exhaustive sweep runs with eviction. `session end` drops the ref scaffolding it accumulated, but only under `ArtifactsMode::Full` where the trace keeps its own copy; the default mode never copies them, and discarding them there would sever snapshot resolution for anyone reading the trace after. `is --property` read element bounds for properties that never use them. BREAKING CHANGE: ENVELOPE_VERSION is now 2.2. `data.complete` is present on every successful snapshot, and a snapshot that exhausts its budget returns `ok: true` with `complete: false` where it previously returned a TIMEOUT error. Callers that branched on TIMEOUT to detect an oversized tree must read `complete` instead. * docs: correct solution docs that contradicted the code and each other A refresh pass over docs/solutions/ against the current tree. Three of the four corrections were internal contradictions that reading the doc alone could not reveal. The pointer-action doc cited `resolve_point_with_deadline`, which has no matches in the repository; hover and drag resolve in two phases, before and under the interaction lease. It also now distinguishes the two hit-tests that exist, since the shared battery's multi-candidate check has grown to cover the click family while the pointer pipeline keeps its own single-point check. The drag-abort doc claimed the release guard "arms only after mouse-down is posted". It arms before, which is what the doc's own prevention rule requires and what the code does. The progressive-snapshot contract described one truncation path. There are two, and the second was silently dropping descendants with no marker at all, violating that doc's own rule. CONCEPTS.md gains Interaction Lease, Partial Observation and Delivery Semantics, and corrects Interaction Policy, which claimed ref commands expose exactly two modes when a third is the base policy of an explicit key press and is directly selectable by language bindings. * fix: only discard refmaps the trace actually copied Being in ArtifactsMode::Full was treated as proof that every refmap had been duplicated into the trace, so ending a session deleted the whole snapshot store. It is not proof. The artifact byte budget rejects a copy once a session's refmaps exceed it, and a serialisation failure skips one too; both report success to the caller. A long full-artifacts session therefore holds snapshots whose only refmap lives in the store, and ending it destroyed them permanently, severing snapshot resolution for anyone reading that trace afterwards. Each snapshot directory is now removed only against its own duplicate in the trace, and the latest-snapshot pointer survives unless the snapshot it names is gone. The existing full-artifacts test encoded the defect: it seeded a snapshot with no trace copy and asserted the store was emptied. It now seeds the copy it claims exists, and a new test covers the case that was losing data — a refmap the trace never copied must survive. |
||
|
|
8f24f04f5a
|
feat: windows vocabulary — roles, states, native_id and name evidence (#115)
Gives the Windows UIA tree its vocabulary: a ControlType→Role map with no catch-all, available actions, a gated state vocabulary, native_id from AutomationId, and name evidence resolved through core's single shared precedence. Core is touched exactly twice; macOS output is byte-identical. Pattern state is read as plain batched properties, each gated on its own Is*PatternAvailable flag — a provider returns a plausible default for a pattern it never implemented, so an ungated read decorates every inert node. invalid and pressed are deliberately unproduced on Windows: the first has no source that is a positive claim, the second no reachable role precondition. Evidence is tri-state throughout. Absent is an answer and Unknown is the lack of one, and the two are never collapsed — a failed read withholds a role, state or affordance rather than granting it. Includes the probe corpus and findings ledger behind these decisions, a census tool that reports shapes and counts without ever serializing a real application's text, a dogfood run against four real UI stacks, and a gate that fails the build when shipped source references the delivery plan. |
||
|
|
41fc178e5e
|
feat: uia element wrapper & tree walk (sub-phase 2.2) (#114)
Fills crates/windows/src/tree/ with the four things every later observation sub-phase consumes: an element wrapper with delegated refcounting, a timeout-bounded UIA client and window-root resolver, a tree walk with a cycle guard and honest completeness, and provider-class-conditional CacheRequest batching. Ships the secure-field gate, an out-of-process fixture, committed COM censuses as dev-box evidence, and corrections to docs/phases.md. Measurement changed the design repeatedly, and each result is a ledger row rather than a silent correction: - A14-3: the end-of-list discriminator is not inverted - code() 0 with result() None, on both builds. - A14-4/A14-9: a dead provider is invisible on both the sibling and property axes, so neither carries liveness. Recorded for 2.4, which owns the snapshot path. - A14-8: IsOffscreen disagrees with itself inside one minimized window, contradicting A1-2 off the managed stack. - A14-10: 2.1's owned-process COM bootstrap is thread-local behind a process-wide guard. - A14-11/A14-12: ElementFromHandle against a non-dispatching window hangs rather than timing out, and the bound that stops it is reachable only from CUIAutomation8. There is no fallback to an unbounded client. - A14-13: UI Automation's client core initialises lazily and not re-entrantly, so concurrent first touches fail with E_FAIL. The accessor serialises that first touch. Every control was observed failing before being claimed: the cycle guard, the discriminator, the secure-field gate, error redaction, capture normalisation, fixture teardown and both hang bounds. ObservationOps::observe_tree still returns PLATFORM_NOT_SUPPORTED; no ref is allocated anywhere in crates/windows. |
||
|
|
00a4282f19
|
fix: write the trace html export through the user-output primitive (#113)
The export hand-rolled temp+rename with a predictable {pid}.{counter}.tmp
name and, on non-unix, a bare fs::write that follows reparse points. The unix
arm opened the temp with create+truncate rather than create_new, so a
pre-created temp at the guessable name kept its own mode while receiving the
trace bytes. Routing through write_user_file adopts the hashed-nonce
create_new temp, destination validation, and cleanup that screenshot and
clipboard output already use.
Normalizing an empty parent in write_atomic_with is required for the swap:
Path::parent yields Some("") for a bare relative filename and metadata("")
fails NotFound, so --out report.html would otherwise be rejected. That bug was
already latent for screenshot --out and clipboard get --out.
Unifies the trace-export test modules on one lock so the process-global
JSON-size clamp cannot race across them.
|
||
|
|
18daaa8215
|
feat: windows toolchain, ci & com bootstrap (sub-phase 2.1) (#112)
* docs: correct the windows sub-phase facts research disproved Sixteen replacements, no annotations. The document is the product's source of truth and now reads true. The COM bootstrap is split by consumer. CoInitializeEx is right for the CLI and wrong for the cdylib, which fails RPC_E_CHANGED_MODE against any host thread already in an STA and whose per-thread balance can never be released from a Drop on another thread; the library path uses CoIncrementMTAUsage, whose cookie is thread-agnostic. MTA is recorded as a requirement rather than a preference, because the single-threaded apartment can prevent handler removal outright. Two things the sub-phase asserted were not reachable. Its private-file seam named a boundary that does not exist: every write site is in core with no adapter handle, and core may not depend on the platform crate. The document now states that constraint and leaves the mechanism to the plan. Its CI paragraph claimed all runners enforce clippy, isolation, and the size cap; only the macOS job does, and the other two lanes each run a single lib invocation. The event invariant said handlers are drained on the dedicated MTA thread. They are not: delivery arrives on several UIA-owned threads at once, the registering worker among them and the main thread never, so handler state has to be safe for concurrent delivery. Runner registration gains the hardening a public repository requires, and gains the measurement it already owed the ledger: it closes the deferred RDP row rather than merely documenting session isolation. The DPI bootstrap records that awareness cannot be verified by read-back, since the query API has no enumerant for the value being set. The ledger's hunk index moves with them, 37 to 40. Hunks backed by research rather than by a probe are labelled as such, so an external citation cannot pass itself off as a measurement. * docs: move the windows runner registration to the sub-phase that needs it Sub-phase 2.1 was to register a self-hosted interactive Windows runner that nothing in 2.1 through 2.11 uses. On a public repository that is a persistently registered runner sitting idle behind ten sub-phases, and GitHub's own guidance is that self-hosted runners should almost never be used for public repositories at all. Registration moves to 2.12, the first sub-phase whose gate needs a real desktop, and it takes the hardening requirements with it: dispatch-only triggering, a written fork-PR approval policy, and an explicit ephemeral-versus-persistent decision. The deferred RDP row moves with it. Its closure was never documentation -- it is a measurement that only exists once a second, non-console session does, so 2.12 now owns closing it rather than 2.1 owning a promise it could not keep. Two probe scripts hardcoded the old closure sub-phase in text they emit. The scripts were corrected and their probes re-run, so the captures were regenerated rather than edited by hand. * docs: plan the windows toolchain, ci, and com bootstrap sub-phase Eight units covering the CI lane extension, Win32 bindings in the platform crate, the COM and DPI bootstrap, the adapter session, the permission probe, and a private-file layer rebuilt from scratch against sub-phase 2.0's measured evidence. Three reviewers found what the first draft got wrong. It installed both the apartment and the private-file seam from ad_init, which the FFI header labels optional and whose own doc promises it initialises nothing -- so any host that skipped it would have run every private write through the unhardened default with nothing to distinguish the two states. The install moves to the site the macOS arm already uses, where a consumer cannot skip it. It also planned a layer that validates security state it never authors. The deleted implementation at least built an owner-only protected descriptor at creation; the rebuild now does too, and rejects reparse points per component the way the unix path rejects symlinks. The ACE validation order was still wrong in the same shape as the defect it exists to prevent: checking the type before forming the SID pointer still dereferences the header at an offset nothing has bounds-checked. The order is now extent-first, with the ACE count treated as advisory rather than trusted. The seam routes at core's five private-file primitives rather than at a list of call sites, which covers the lock and liveness paths a list had missed and needs no call-site edits at all. * docs: cut acl validation from the windows private-file design Reverses the ancestor-walk decision on measurement. A plain leaf under the user profile already inherits SYSTEM, Administrators and the user with no Users entry, all inherited and none explicit, so the walk would verify a property Windows already guarantees. Unix has to author its permission because the default there is world-readable; Windows does not, because the default is already private. The only principal the walk would additionally exclude is another administrator, who holds SeTakeOwnershipPrivilege and can seize any object whatever its DACL. Building a chain-walking validator against someone who can bypass it costs more than it buys, and the cost is specific: parsing ACEs is what the previous layer did, and its AceSize handling is the defect that took the layer down. Removing the parser removes the bug class rather than fixing it, so no successor module exists and a test asserts the ACE symbols never come back. What survives is the control with real unix parity and a real attack behind it: per-component reparse-point rejection. That is an integrity control, not a confidentiality one -- a junction planted on the path redirects where the product writes, which no permission on the intended destination prevents. Cross-admin isolation is now answered rather than open: out of scope, for the same reason macOS never raised the question. * test: resolve the test binary per platform via CARGO_BIN_EXE * feat: add target-gated windows-sys bindings to the windows crate * feat: route private-file primitives through a core-defined platform seam * feat: establish com apartment and dpi awareness at unskippable call sites * feat: report windows permissions truthfully from a host-independent seam * ci: extend the windows lane to the full package surface * feat: give the windows adapter a session that owns apartment lifetime * feat: rebuild windows private-file hardening behind the core seam * test: pin the inherited-acl assumption module-free on a real profile leaf * feat: install the windows private-file seam at both unskippable consumers * fix: scope the temp lease per write and fold duplicated windows helpers * fix: route windows session and trace directory creation through the private-file seam * fix: scope the com bootstrap to adapter commands and harden review-flagged tests * test: cover the owner, locality, replace-acl, and sweep-race branches of the windows hardening * fix: pin the private-file directory chain to close the junction-swap window * docs: settle the user-output seam exclusion and pin it with a test * refactor: split the trace file plumbing and ffi adapter tests into sibling modules |
||
|
|
5fd9543a79
|
feat: add windows platform exploration probe corpus (phase 2.0) (#111)
* 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. |
||
|
|
4fa7661608 |
feat: open the Windows adapter phase with corrected platform facts
Establishes feat/windows-adapter as the base branch for all Windows work. Sub-phases 2.0 through 2.15 are cut from here and merge back here; main stays the macOS-GA line and gains Windows once, at the end, when the adapter is production-solid as a whole. Corrects the Phase 2 Windows sections, which were written from documentation research in 2026-04 and re-verified against current sources and a real Windows machine: - uiautomation 0.25 (a "0.24" requirement will not resolve to it under 0.x semver); windows-capture 2.0.0 - the recorded 1.5.4 was never published - Chromium 138 ships native UIA by default, so the web-wrapper depth-skip is the primary lever and --force-renderer-accessibility is a fallback - Windows 11 22H2+ moved tray overflow to TopLevelWindowForOverflowXamlIsland - windows-11-arm runners have been GA for public repos since 2025-08-07 - Windows 10 1809 remains the API floor, now qualified with its servicing reality (LTSC 2019 / Server 2019 are the serviced targets) - Notepad is two different apps across Server and Win11 client SKUs - CI tables reflect the real test-windows and test-linux lanes from v0.6.0 Adds the no-convenience-deferral rule: scope moves between sub-phases of a phase, never out of it. The only sanctioned deferral is proven impossibility, evidenced by a ledger row, shipping an honest PLATFORM_NOT_SUPPORTED. 2.14 and 3.14 are no longer deferrable stretch sub-phases. |
||
|
|
dde0b98dc0
|
chore: track planning artifacts with secret and privacy scanning (#109)
Some checks failed
CI / Format (push) Has been cancelled
CI / Rust 1.89 MSRV (push) Has been cancelled
CI / Native check (macOS) (push) Has been cancelled
CI / Native check (Linux) (push) Has been cancelled
CI / Native check (Windows) (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / FFI Python Smoke (push) Has been cancelled
CI / FFI Header Drift (push) Has been cancelled
CI / FFI Panic Guard (push) Has been cancelled
CI / FFI Stub-Adapter Passthrough (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / FFI Release Gates (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
Tracks docs/plans and docs/brainstorms, adds .gitleaks.toml with privacy rules, and runs a history-mode gitleaks scan in CI. Docs and tooling only - no releasable change. |
||
|
|
33bc47ca38
|
chore(main): release 0.6.0 (#107) | ||
|
|
8ad66b8f21
|
refactor!: remove speculative Win32 private-file layer from core, add real Windows/Linux test lanes (#106) | ||
|
|
3f3d69a863
|
test: widen wall-clock budgets on retry/hydration tests to stop CI flakes (#103)
Some checks failed
CI / Format (push) Has been cancelled
CI / Rust 1.89 MSRV (push) Has been cancelled
CI / Native check (macOS) (push) Has been cancelled
CI / Native check (Linux) (push) Has been cancelled
CI / Native check (Windows) (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / FFI Python Smoke (push) Has been cancelled
CI / FFI Header Drift (push) Has been cancelled
CI / FFI Panic Guard (push) Has been cancelled
CI / FFI Stub-Adapter Passthrough (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / FFI Release Gates (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
A class of unit tests bounds real retry/resolve/hydration work with a tight wall-clock deadline (100-1000 ms) and asserts a definite outcome (success, or a specific terminal code after a bounded retry). On slow, contended GitHub runners the work overran the budget and the test got a Timeout instead of its asserted outcome, flaking the Test job while every local run passed. Widen those budgets to the suite's 5 s idiom so the asserted state is reached deterministically. Genuine deadline-expiry tests (permanently-unresolvable adapters, single-shot timeout_ms 0, deadline-value plumbing, lock-contention) keep their tight budgets - a slow runner still expires them correctly. Test-only; no production change. |
||
|
|
24ba049ec4
|
chore(main): release 0.5.0 (#100)
* chore(main): release 0.5.0 * chore: sync cargo lock for release 1.89 * docs: curate 0.5.0 changelog Drop the stale '## Unreleased' block (the sessions/#89 refactor already shipped in v0.4.6 and was duplicated above the new entry), and expand the foundation-contract feature entry with the headline new capabilities so the release communicates what callers gain, not only what breaks. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Lahfir <nmhlahfir2@gmail.com> |
||
|
|
3f322728b4
|
feat!: implement Playwright-grade foundation contract
Settle the Playwright-grade reliability contract in agent-desktop-core before the Windows/Linux adapters are built, so they inherit it instead of redesigning it. Every command now observes, waits, verifies, and reports honestly instead of firing blindly. Highlights: capability-supertrait split of PlatformAdapter with not_supported() defaults; canonical role/state vocabulary with live `is --property visible`; display enumeration (`list-displays`) and honest `--screen` with scale factor; truthful Automation permission; `native_id` identity spine; window-id-first resolution; serializable `LocatorQuery` with live `find`; default-on auto-wait before every ref action; three-way `hit_test` occlusion gate; `scroll_into_view` in core; core accessible-name precedence; typed `ActionStep` delivery tier; `ProcessState` and `APP_UNRESPONSIVE`; `LaunchOptions`; baseline-diff desktop signals (`wait --event`); typed clipboard (`Text`/`Image`/`FileUrls`); mouse modifier chords and `mouse-wheel`. Hardened through a 35-reviewer pass with independent validation and a green live e2e gate (109/0), plus a head-vs-main performance comparison harness. BREAKING CHANGE: default-on auto-wait changes the timing of every previously-untouched ref-action call (bounded 5000 ms default; `--timeout-ms 0` restores single-shot). `ENVELOPE_VERSION` is now `2.1` (adds the `APP_UNRESPONSIVE` code and process state in error details). FFI ABI major is `3` (append-only struct evolution; `wait --event` is intentionally not exposed over FFI). The legacy string clipboard API is removed in favor of typed content. `key-down`/`key-up` fail closed until daemon-owned held input exists. `close-app` verifies termination and the osascript fallback path is removed. `--text` matching is subtree containment: `find --text X --first` returns the outermost matching container. |
||
|
|
52705afbe1
|
chore(main): release 0.4.7 (#92)
Some checks failed
CI / Format (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / FFI Python Smoke (push) Has been cancelled
CI / FFI Header Drift (push) Has been cancelled
CI / FFI Panic Guard (push) Has been cancelled
CI / FFI Stub-Adapter Passthrough (push) Has been cancelled
CI / FFI Codegen Drift (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / FFI Release Gates (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
* chore(main): release 0.4.7 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
e3e1872ff3
|
feat: add trace viewer and replay artifacts
Adds the trace read/replay layer on the session-first foundation: `trace show` merges per-process segments into one deterministic timeline (bounded JSON for agents), and `trace export` renders a single self-contained, XSS-safe HTML viewer for humans. Opt-in `session start --screenshots` captures pre/post-action screenshots and refmap copies; command.start/end boundary events and a versioned trace.meta header make a step-by-step replay reconstructable. Redaction is hardened so raw caller arguments never leak into trace-reachable error messages. Available across CLI, batch, and FFI. |
||
|
|
e16b218653
|
chore(deps): bump the github-actions group with 3 updates (#88)
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CI / FFI Python Smoke (push) Waiting to run
CI / FFI Header Drift (push) Waiting to run
CI / FFI Panic Guard (push) Waiting to run
CI / FFI Stub-Adapter Passthrough (push) Waiting to run
CI / FFI Codegen Drift (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / FFI Release Gates (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
Bumps the github-actions group with 3 updates: [actions/cache](https://github.com/actions/cache), [actions/setup-python](https://github.com/actions/setup-python) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/cache` from 6.0.0 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits]( |
||
|
|
b9e63fc21e
|
chore(main): release 0.4.6 (#90)
* chore(main): release 0.4.6 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
35fa914b52
|
feat: make sessions the first-class trace container
Sessions now own tracing: `session start` creates a manifest-gated trace sink with per-process JSONL segments under ~/.agent-desktop/sessions/<id>/trace/, so agents set the session once instead of passing --trace on every command. Bare --session stays snapshot-namespace-only; --trace <path> still overrides for CI and one-offs. Adds session start/end/list/gc, activation resolution (flag > AGENT_DESKTOP_SESSION > pointer), FFI verification tests, and status fields for session_id + tracing. |
||
|
|
c454f4ab1c
|
chore(main): release 0.4.5 (#87)
Some checks failed
CI / Format (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / FFI Python Smoke (push) Has been cancelled
CI / FFI Header Drift (push) Has been cancelled
CI / FFI Panic Guard (push) Has been cancelled
CI / FFI Stub-Adapter Passthrough (push) Has been cancelled
CI / FFI Codegen Drift (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / FFI Release Gates (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
* chore(main): release 0.4.5 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
ce232787b5
|
feat: add --wait-for selector polling flags (#86)
Global --wait-for / --wait-for-gone (-w) and --wait-timeout flags that poll the accessibility tree until a role:text selector appears or disappears, then return the snapshot. Honored by snapshot and the 16 ref-action commands; post-action waits scope to the acted-on window. Closes #84. |
||
|
|
d81ce7ba2c
|
chore(main): release 0.4.4 (#85)
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CI / FFI Python Smoke (push) Waiting to run
CI / FFI Header Drift (push) Waiting to run
CI / FFI Panic Guard (push) Waiting to run
CI / FFI Stub-Adapter Passthrough (push) Waiting to run
CI / FFI Codegen Drift (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / FFI Release Gates (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
* chore(main): release 0.4.4 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
94ce6c551f
|
feat(macos,core): harden adapter and core foundation with caller-controllable guardrails (#82)
Hardens the macOS adapter and platform-neutral core: stuck-key/clipboard/scroll/click fixes, deep-copy clipboard restore, notification-session and resolve-deadline hardening, blocked-combo guardrail moved to the adapter with a --force caller override, plus ~60 behavior-pinning tests. No breaking changes. |
||
|
|
b00a967e0b
|
docs: add community health files for GitHub community standards (#83)
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CI / FFI Python Smoke (push) Waiting to run
CI / FFI Header Drift (push) Waiting to run
CI / FFI Panic Guard (push) Waiting to run
CI / FFI Stub-Adapter Passthrough (push) Waiting to run
CI / FFI Codegen Drift (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / FFI Release Gates (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
Completes the recommended community profile (opensource.guide best practices): - CODE_OF_CONDUCT.md — Contributor Covenant v2.1 - CONTRIBUTING.md — repo-accurate dev workflow, quality gates, coding standards, conventional-commit and command-extensibility guidance - SUPPORT.md — where to get help / report issues - .github/ISSUE_TEMPLATE/ — bug + feature issue forms and config (security and docs contact links; blank issues disabled) - .github/PULL_REQUEST_TEMPLATE.md — summary, type, gate checklist |
||
|
|
5cb71051f6
|
chore(main): release 0.4.3 (#81)
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CI / FFI Python Smoke (push) Waiting to run
CI / FFI Header Drift (push) Waiting to run
CI / FFI Panic Guard (push) Waiting to run
CI / FFI Stub-Adapter Passthrough (push) Waiting to run
CI / FFI Codegen Drift (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / FFI Release Gates (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
* chore(main): release 0.4.3 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
a708fa0332
|
fix(macos): harden retained_handle null guard against release-only CFRetain(null) (#80)
Make the retained_handle null check unconditional so release builds no longer risk CFRetain(null); tests keep benign placeholder behavior, production returns ElementNotFound. |
||
|
|
cadbdf9602
|
chore(main): release 0.4.2
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CI / FFI Python Smoke (push) Waiting to run
CI / FFI Header Drift (push) Waiting to run
CI / FFI Panic Guard (push) Waiting to run
CI / FFI Stub-Adapter Passthrough (push) Waiting to run
CI / FFI Codegen Drift (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / FFI Release Gates (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
Release 0.4.2 — Phase B and C of the FFI completion (Python smoke harness, cross-platform parity gates, build.rs codegen). |
||
|
|
9023f331b3
|
feat(ffi): Phase B and C — Python smoke harness, parity gates, build.rs codegen (#77)
Completes the FFI plan: U9 adds a Python ctypes smoke harness proving the C ABI from a non-Rust host; U10 adds cross-platform parity CI gates for header drift, codegen drift, panic-unwind, and stub-adapter passthrough; U11 generates the command-backed wrappers from build.rs templates with a committed drift-gated output. The C ABI stays byte-identical and the surface is Windows and Linux ready with no new FFI code. |
||
|
|
6e86f1c299
|
chore(main): release 0.4.1
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
|
||
|
|
8de8f904db
|
feat: complete FFI C-ABI surface (Phase A) — handshake, pipeline entrypoints, log callback (#67)
* build: make cbindgen regenerate the C ABI static-assert guards
Add [const] allow_static_const = false to cbindgen.toml so every
pub const AD_*_SIZE emits as a #define macro (a C constant expression)
rather than static const (which is not valid inside _Static_assert).
Add a cbindgen.toml trailer with all 19 C11 ABI guards (6 sizeof, 6
_Alignof, 7 offsetof). Each sizeof guard references the corresponding
AD_*_SIZE macro so the size literal lives in exactly one place: the
Rust source. The Rust source already has compile-time asserts tying each
const to its struct, so the chain is: Rust const → #define macro →
_Static_assert → C compile gate.
Rename pub const MAX_C_STRING_BYTES to AD_MAX_STRING_BYTES so the
public ABI name matches the header macro (#define AD_MAX_STRING_BYTES)
and the internal const name no longer leaks. Update all call sites and
the describe() error string.
Pin cbindgen 0.29.4 in scripts/update-ffi-header.sh so regen is
reproducible.
* test: stabilize flaky wait-resolution test under CI load
element_wait_retries_transient_ambiguous_resolution and its sibling used a
250ms budget that the happy path clears instantly but a loaded CI runner
(full --lib --workspace in parallel) can exceed before the transient
AmbiguousTarget retry resolves — an intermittent red CI unrelated to any
code change. Raise the ceiling to 2000ms; the success path still returns on
the second resolve, so the test stays fast while becoming load-robust.
* refactor: enforce cbindgen version in update-ffi-header.sh
The script documented 0.29.4 as required but accepted any installed
version silently. A mismatched cbindgen can produce a semantically
different header with no diagnostic. Fail fast with a clear install
command if the version does not match exactly.
* feat: add ad_abi_version and ad_init ABI handshake
* refactor: address review on ad_abi_version
* feat: add ad_version FFI entrypoint
* refactor: move guard_non_null inside trap_panic for convention parity
All other FFI entrypoints validate input pointers as the first statement
inside trap_panic(|| unsafe { … }). ad_version placed the guard outside
the closure; align it with the uniform convention throughout the crate.
* feat: add session-scoped FFI adapter constructor
* refactor: address review on session context
* feat: add ad_set_log_callback with tracing layer
* refactor: address review on log callback
- Tighten core visibility: revert `pub mod trace` to `pub(crate) mod trace`,
re-export only `sanitize_trace_value` at the crate root so the FFI crate
imports it without exposing `TraceConfig` and internal helpers publicly
- Add per-thread re-entrancy guard (`IN_CALLBACK` + `CallbackGuard` RAII
drop) to `on_event`: a consumer callback that emits `tracing` events now
silently drops the recursive invocation instead of risking a stack overflow
that `catch_unwind` cannot stop
- Update module doc and `ad_set_log_callback` `///` doc to document the
re-entrancy protection; regenerate committed FFI header via cbindgen
* feat: add ad_snapshot with refmap pipeline
* refactor: address review on ad_snapshot
Promote app_error_to_adapter to pub(crate) in commands/mod.rs so future
command files share one conversion instead of copying the match per file.
Document the tri-state *out contract (null on arg/infra errors, populated
JSON envelope on command-level errors) in both the Rust doc comment and
the C header.
* feat: add ad_status FFI entrypoint
* refactor: address review on ad_status
* feat: add ad_wait with size-pinned AdWaitArgs
* refactor: DRY ad_wait field decoding
* fix: align command_context error return with errno-style last-error invariant
The context failure arm in ad_wait hardcoded ErrInternal while
set_last_error stored the actual mapped code; callers relying on
ad_last_error_code() == return value would see a mismatch. Mirror the
execute() Err arm and return last_error_code() instead.
* feat: add ad_execute_by_ref with strict resolution
* refactor: address review on ad_execute_by_ref
- extract app_error_to_adapter to error.rs pub(crate), removing
duplicate copies from snapshot.rs and execute_by_ref.rs
- replace 24-line tri-state ref_id decode with required_adapter_string
- use last_error_code() on validate_ref_id failure path for consistency
- fix *out dual-channel doc (null on guard/decode failure, non-null
JSON envelope on command-level errors) in both Rust doc and C header
- rename misleading test: stale_ref_returns_error → returns_error_envelope
(missing refmap surfaces SNAPSHOT_NOT_FOUND, not STALE_REF)
* docs: restore ABI header comments lost in cbindgen regen
Add /// docs on the Rust source so cbindgen re-emits the privacy note on
ad_last_error_details, the AdResult forward-compat note, and the behavioral
descriptions for the ad_execute_action* family.
* refactor: dedup ad_wait error conversion via shared helper
Replace wait.rs's local app_error_to_adapter_error with the shared
commands::app_error_to_adapter, giving one canonical AppError->AdapterError
conversion across every ffi command.
* fix: harden ABI assert trailer against double-include, document panic guard
Add a one-shot #ifndef AGENT_DESKTOP_ABI_ASSERTS guard around the
_Static_assert trailer in cbindgen.toml so re-including agent_desktop.h
in a single translation unit is unambiguously safe in all C standards
(C11 already allows repeated file-scope _Static_assert, but the guard
makes the property explicit and unconditional).
Add c_header_double_include.rs regression test that includes the committed
header twice via the system cc and asserts the compile succeeds, mirroring
the existing c_header_compile.rs harness pattern.
For Finding #14 (panic=abort guard): empirical probe confirms that Cargo
always sets CARGO_CFG_PANIC="unwind" in the build script for this crate
regardless of the active profile's panic setting — because the crate
declares both cdylib and rlib crate-types. A build-time check would be
silently inert. Document the invariant and the infeasibility of the
CARGO_CFG_PANIC guard in build.rs module docs instead; the release-ffi
profile enforces unwind at the profile level.
* fix(ffi): surface foreign-subscriber conflict, soften ad_init wording, direct agents to ad_snapshot
Finding #15: replace discarded try_init result with OnceLock<bool> so the first
ad_set_log_callback call with a non-null callback returns ErrInternal when a
foreign global subscriber already owns the process, rather than silently no-oping.
Re-registration and NULL-unregister paths are unaffected. Adds three unit tests
covering the new routing logic.
Finding #8: soften ad_init and AD_ABI_VERSION_MAJOR rustdoc from mandatory
pre-call requirement to recommended ABI-compatibility check; behaviour unchanged.
Finding #12: rewrite ad_get_tree rustdoc to direct observe-act agents to
ad_snapshot (refs, refmap, JSON envelope) and reserve ad_get_tree for ref-less
raw-tree consumers; removes stale "invoke the CLI" guidance.
* refactor: unify FFI command envelope serialisation and fix wait/status error contract
Extract write_command_envelope into commands/envelope_out.rs, eliminating
the ~35-line guard→serialize→string_to_c block duplicated across all five
command modules (snapshot, execute_by_ref, wait, status, version).
ad_wait and ad_status now write the error JSON envelope into *out on
command-level failures (TIMEOUT, ELEMENT_NOT_FOUND, etc.), matching the
behaviour already present in ad_snapshot and ad_execute_by_ref. Guard and
infrastructure rejections (null adapter/out/args, off-main-thread, invalid
UTF-8) continue to leave *out null — the infra/command boundary is
preserved exactly.
Add ad_wait_command_error_writes_error_envelope_into_out to verify the
unified contract: a zero-timeout element wait that cannot be satisfied must
produce an ok:false envelope in *out, not a null pointer.
* test: add unit tests to write_command_envelope proving #3 contract
* build: regenerate FFI header after round-1 review fixes
* docs(ffi): document ownership/lifetime contracts on adapter destroy and wait
Add Safety note to ad_adapter_destroy that callers must not destroy the
handle while any call on it is in flight on another thread — concurrent
destroy + in-flight call is use-after-free (confirmed reachable: destroy
is main-thread-exempt while ad_wait holds &*adapter through its blocking
wait::execute loop).
Add blocking-duration and adapter-lifetime note to ad_wait.
Extend ad_snapshot's existing partial note to cover explicit window
targeting (window_id, not yet ABI-exposed) alongside skeleton/drill-down,
and add CLI guidance for agents that need those features today.
* refactor(ffi): route ad_execute_by_ref through canonical core pipeline
Fix four coupled architectural findings in the FFI execute_by_ref path:
#2 (P1): Delete bespoke run_ref_action that bypassed CommandContext tracing
and hard-coded ref_id "<ffi>". Route ad_execute_by_ref through a new
commands::execute_by_ref::execute in core, which calls the same
execute_ref_action_with_context pipeline the CLI click/type/etc. commands
use — full trace, strict resolution, and actionability preflight.
#7 (P2): Add nullable snapshot_id: *const c_char parameter as the third
argument. Tri-state: null → latest snapshot (prior behaviour), valid UTF-8
→ pin that snapshot id, non-null invalid UTF-8 → ErrInvalidArgs. Passes
snapshot_str.as_deref() to RefArgs.snapshot_id mirroring CLI --snapshot
semantics. Update all 7 call sites in c_abi_lifecycle.rs and the extern
decl in tests/common/mod.rs (c_abi_actions.rs had no sites).
#10 (P2): Make core the single source of policy truth. Add
Action::base_interaction_policy() (delegates to may_use_focus_fallback —
TypeText + PressKey → focus_fallback, everything else → headless) and
InteractionPolicy::join(self, other) (bitwise-OR on the two capability
flags: elevate-only, never downgrade). FFI maps AdPolicyKind → core
InteractionPolicy and calls base.join(caller_ip); core::execute_by_ref
does the base+elevation. Delete the bespoke effective_action_policy fn and
its six duplicated unit tests from the FFI; repointed truth-table coverage
lives in the six FFI tests that delegate to core and the join unit tests in
interaction_policy.rs. Note: PressKey's base shifts headless→focus_fallback
vs the old FFI-only fn — intentional alignment with the full CLI table.
#6 (P1, light): Success and error paths already route through the shared
write_command_envelope helper (no reintroduction of bespoke serialization).
Add a /// note on ad_execute_by_ref documenting dispatch-before-serialize
ordering so callers understand the near-impossible ErrInternal-after-action
scenario without requiring heavy pre-validation machinery.
* build: regenerate FFI header after core-routing fixes
* test(ffi): split c_abi_lifecycle and add observe→act roundtrip tests
Finding #4: c_abi_lifecycle.rs was 1127 LOC, violating the 400-LOC cap.
Extracted focused modules each well under 400 lines:
- c_abi_init.rs (54 LOC) — ad_abi_version / ad_init
- c_abi_session.rs (88 LOC) — session adapter ctor
- c_abi_log_callback.rs(182 LOC) — log callback tests + statics
- c_abi_json_commands.rs(165 LOC) — ad_version / ad_status
- c_abi_snapshot.rs (131 LOC) — ad_snapshot guard + envelope tests
- c_abi_wait.rs (178 LOC) — ad_wait guard + error-envelope tests
- c_abi_execute_by_ref.rs(167 LOC) — ad_execute_by_ref guard tests
c_abi_lifecycle.rs shrunk to 176 LOC (null-safety + list lifecycle).
Finding #5: add snapshot→execute_by_ref roundtrip tests.
- stale_ref_returns_ok_false_error_envelope: always runs in CI; sets a
temp HOME (empty refmap), calls ad_execute_by_ref(@e1), asserts
ok:false + error.code when the command path executes, tolerates
ErrInternal/null-out for the macOS main-thread guard.
- snapshot_execute_by_ref_live_roundtrip: #[ignore]; needs AX permission,
a live app, and main-thread execution (E2E harness).
* fix(ffi): zero ad_wait *out before args guard; correct PressKey policy docs
Review of the review fixes:
- ad_wait now zeroes *out before the args null-check so a null-args
rejection honours the documented *out-zeroed contract (matches the
other command entrypoints).
- Corrected the PressKey policy doc wording: focus_fallback is the
shared base from Action::base_interaction_policy, not a claim of CLI
ref-PressKey parity (no CLI ref-PressKey action exists).
* chore: remove stray rust_out build artifact and gitignore it
* fix(ffi): thread session context through legacy ref-action path and correct doc steering
Add execute_entry_with_context to core so the FFI legacy path
(ad_execute_ref_action_with_policy) threads the adapter's real
CommandContext — built from the session id set at adapter creation —
instead of CommandContext::default(). The existing execute_entry
delegates to it with default() so all other callers are unchanged.
Rewrite the /// docs on all three native-handle / legacy-struct
entrypoints (ad_execute_action, ad_execute_action_with_policy,
ad_execute_ref_action_with_policy) to remove the false "CLI parity"
steering and describe each accurately as a low-level escape hatch with
verbatim-policy dispatch. Point observe→act callers at ad_execute_by_ref.
Add core tests confirming execute_entry_with_context succeeds, delegates
correctly, and emits trace events when a trace path is configured. Add FFI
test confirming the session-adapter path resolves to the same error class
as the no-session path (session id is wired into trace, not the error
surface).
* refactor: unify CLI ref-action policy via CommandContext::request_base
All 16 ref-action commands previously hardcoded InteractionPolicy literals
that duplicated the intent already encoded in Action::base_interaction_policy().
Add CommandContext::request_base(action) which reads the canonical base from
the action itself and delegates to request(), then replace every hardcoded
call site so there is a single policy source of truth for CLI and FFI.
Update the all_context_request_callers_are_policy_tested guard to search for
context.request_base( rather than context.request(.
Behavior is byte-identical: headless() and focus_fallback() literals were
exact mirrors of what base_interaction_policy() returns for each action.
* docs(ffi): fix Safety doc, add header preamble, document live-test deferral
Finding #9: correct the # Safety section on ad_execute_by_ref to distinguish
ref_id (non-null required; null is defined but rejected with ErrInvalidArgs)
from snapshot_id (null is meaningful — latest snapshot). The param tri-state
doc was already accurate; only the Safety section implied false symmetry.
Finding #10: add an agent-workflow orientation preamble to cbindgen.toml via
the after_includes key. The 13-line C block-comment lands after sys-includes
and before declarations (inside the include guard), covering the full
ad_init → ad_adapter_create → ad_snapshot → ad_execute_by_ref →
ad_free_string / ad_adapter_destroy loop with the macOS main-thread
requirement. Verified via local cbindgen 0.29.4 regeneration; header not
committed (orchestrator regenerates).
Findings #2/#8: expand module doc and live-roundtrip #[ignore] test doc to
precisely explain why the full observe→act loop cannot run under libtest on
macOS (off-main-thread scheduler + AX guard), how to run manually, and that
the full-loop CI proof is deferred to plan unit U9 / Phase B (Python ctypes
external-consumer harness). Strengthen the always-running error-envelope test
with an error.message assertion (guaranteed by the error contract) without
pinning error.code (which varies by load path; pinning would cause flakiness).
* build: regenerate FFI header after round-2 review fixes
* fix: correct FFI doc/test gaps found in PR #67 audit
#7: replace non-existent ad_click/ad_type_text references in ad_get_tree
rustdoc with the real entrypoint (ad_execute_by_ref + AdAction).
#6: extend cbindgen.toml preamble with snapshot_id round-trip (pass
data.snapshot_id back to pin snapshot vs NULL for latest), AdAction
construction (zero-init + set kind + kind-specific fields), and policy
semantics (0=keeps built-in base, 2=Headed allows cursor/focus fallbacks).
#2: add dispatch-before-serialize note to ad_snapshot matching the
existing note on ad_execute_by_ref — refmap is written before JSON
serialisation, so a serialisation failure leaves *out null + ErrInternal
while the refmap is already on disk.
#10: add out-of-range policy test in c_abi_execute_by_ref.rs that passes
policy=99 and asserts ErrInvalidArgs|ErrInternal + out null, with the
last-error assertion gated on ErrInvalidArgs to tolerate macOS off-main-
thread CI returning ErrInternal before the policy check is reached.
* fix: make error suggestions transport-neutral and populate retry_command
Finding #4: STALE_REF, AMBIGUOUS_TARGET, ELEMENT_NOT_FOUND, SNAPSHOT_NOT_FOUND,
NOTIFICATION_NOT_FOUND, and POLICY_DENIED suggestion strings previously referenced
CLI-only flags (--skeleton, --snapshot, --session, --headed) or bare CLI subcommand
syntax (snapshot, list-notifications), making them meaningless to FFI consumers.
Rewritten to name the operation neutrally with both CLI and FFI equivalents in
parentheses.
Finding #15: ErrorPayload::retry_command was always None; the with_retry builder
existed but was never called from from_app_error. Added retry_token_for_code() to
populate the field for mechanically retryable codes: STALE_REF and SNAPSHOT_NOT_FOUND
get "snapshot;execute_by_ref" (re-snapshot then re-execute by ref); POLICY_DENIED gets
"escalate_policy" (policy escalation, not a re-run of the same call). All other codes
remain None to avoid misleading auto-retry loops.
Tests updated: stale_ref_suggestion_mentions_skeleton renamed and tightened to assert
transport-neutral content; snapshot_ref_tests.rs skeleton assertion updated to match;
four new output.rs tests cover the populated tokens and the None cases.
* fix(trace): wire session_id into every JSONL record, improve FFI ref label, drop duplicate test
#9: thread session_id (Option<&str>) through TraceConfig::emit/emit_lazy/write_event and
insert it as a top-level field after the sanitized fields block. This makes the docs true
for everyone (CLI + FFI when both --session and --trace are provided). session_id is not
in SENSITIVE_KEYS and is intentionally inserted after sanitize_trace_value so it cannot
be shadowed or redacted. Omitted entirely when context has no session (no null key).
#8: replace the opaque "<ffi>" ref label in execute_entry_with_context with a
role/path-index-derived label (e.g. "<button/2/0/3>"). Uses only role and numeric path
indices — no content fields — so the label is safe to emit in the unredacted "ref"
trace key. Improves per-record correlation in multi-element FFI trace logs.
#14: delete execute_entry_delegates_to_entry_with_context from ref_action_tests.rs.
The test was byte-identical in assertions to failed_action_still_releases_resolved_handle
and added no new coverage. Removed rather than forced a weak rewrite.
Tests added/updated:
- context.rs: trace_injects_session_id_as_top_level_unredacted_field (session set),
trace_writes_jsonl_without_stdout_dependency extended with session_id absence assert
- ref_action_tests.rs: execute_entry_with_context_emits_trace_events extended to assert
session_id in every emitted record; trace_records_omit_session_id_when_context_has_none;
ref_label_from_entry_uses_role_and_path_indices
* fix(core): make stale-ref actionability suggestion transport-neutral; regen header
Mirrors the round-3 transport-neutral suggestion rewrite for the
live-staleness path in actionability, plus the regenerated FFI header
carrying the round-3 doc/preamble updates.
* refactor: dedup ffi policy mapping and error conversion helpers
Replace the duplicated AdPolicyKind→InteractionPolicy three-arm match in
execute_by_ref.rs with a canonical to_interaction_policy() method on
AdPolicyKind. Delegate action_request() in actions/execute.rs to the same
method, replacing its own redundant match. Replace the inline AppError→
AdapterError match in ad_execute_ref_action_with_policy with the shared
app_error_to_adapter helper already used everywhere else.
* refactor(ffi): apply ponytail/simplify cuts (then_some, trim core doc)
Use bool::then_some for ad_wait optional scalars; drop the FFI-history
sentence from the core execute_by_ref doc (the FFI doc retains it for C
binding authors). Behavior unchanged.
|
||
|
|
4a6d8aa308
|
chore(deps): bump the github-actions group across 1 directory with 3 updates (#65)
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/cache](https://github.com/actions/cache) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action). Updates `actions/checkout` from 6.0.3 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
d560d59945
|
chore(main): release 0.4.0 (#66)
* chore(main): release 0.4.0 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
dbb2be639e
|
refactor!: over-engineering audit cleanup (#64)
* refactor!: drop vestigial version --json flag and dead code
The tool emits a JSON envelope for every command and has no plain-text
mode, so the version --json flag toggled nothing. Remove it end-to-end:
CLI flag, core VersionArgs, batch path, dispatch/main call sites, and the
snapshot test. Correct the stale skill docs (they documented a non-existent
plain-string default and wrong platform/arch keys; actual keys are
version/target/os, always inside the JSON envelope).
Also: delete dead code (AppContext, WindowContext, Response::with_app and
their re-export; core BatchArgs); inline single-caller helpers
(strip_ref_bounds_when_hidden, helpers::find_window_for_pid,
permissions::render); use libc::kill instead of a hand-declared extern in
refs_lock.
BREAKING CHANGE: the version command no longer accepts --json; it always
emits the standard JSON envelope.
* refactor: inline single-caller ffi conversions, drop dead from_c
Inline direction_from_c and snapshot_surface_to_core into their sole
callers. Remove AdImageFormat::from_c (production converts Rust->C only;
the C->Rust direction was dead, #[allow(dead_code)]) together with its
unit test and fuzz line. Drop the ErrorCode/AdResult cardinality assert:
the exhaustive error_code_to_result match plus the per-discriminant pins
already enforce the ABI invariant.
* refactor: dedup keycode table, inline single-caller macos helpers
key_to_keycode delegated the same 52-entry table that
keyboard_map::key_name_to_code already owns; delegate to it, guarding the
modifier names so a modifier passed as the primary key still resolves to
None (preserving existing behavior). Inline windows_from_records,
is_window_candidate, make_output, and the normalized_role_for_element
wrapper (which let one caller classify the same element twice); collapse
accessibility_report_state to its one-line form. Swap four local
HashSet dedup sets to FxHashSet (rustc-hash, already a dependency).
* chore: mark internal crates publish = false
These crates are workspace-internal (consumed only via path deps; CI never runs cargo publish). Marking them unpublishable prevents accidental crates.io release and makes removing unreferenced pub items unambiguously safe.
* fix: finish version --json removal in ci smoke test and help text
The version --json flag was dropped earlier but two references survived:
the npm smoke-test in ci.yml (which now errored on the unknown flag and
failed the job) and the --help text that still advertised it. Point both
at plain version, which emits the same JSON envelope. Add a batch
regression test asserting version now rejects args, and drop the
misleading {"json":true} from the stop-on-error fixture.
* test: guard ErrorCode<->AdResult bijection after counter removal
Code review flagged that dropping the cardinality counters left reverse
drift (a new AdResult error variant without a matching ErrorCode)
unguarded. The counters only weakly approximated this anyway: they
compared two hand-maintained variant arrays to each other, decoupled
from the real enums.
Replace them with a stronger, leaner guard: an exhaustive reverse match
(error_code_origin) that fails to compile if an AdResult error variant
is added without declaring its ErrorCode origin, plus a round-trip test
asserting the forward and reverse maps agree -- a consistency check the
counters never performed. Document the bijection contract on
error_code_to_result.
|
||
|
|
93a4bb1e2a
|
chore(main): release 0.3.1 (#63)
Some checks failed
CI / Format (push) Has been cancelled
CI / Test (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
* chore(main): release 0.3.1 * chore: sync cargo lock for release 1.85 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
9f144c2caf
|
fix: harden macos stale ref resolution (#62)
* fix: harden macos stale ref resolution * fix: tighten stale ref fallback resolution * fix: fail closed for gone titled windows * refactor: tidy stale ref resolver naming |
||
|
|
906deec406 |
docs: update bundled skill versions
Some checks are pending
CI / Format (push) Waiting to run
CI / Test (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
Release / Release Please (push) Waiting to run
Release / Build (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (aarch64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-apple-darwin) (push) Blocked by required conditions
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Blocked by required conditions
Release / Build FFI (x86_64-pc-windows-msvc) (push) Blocked by required conditions
Release / Publish to GitHub Release (push) Blocked by required conditions
Release / Publish to npm (push) Blocked by required conditions
Release / Publish Skills to ClawHub (push) Blocked by required conditions
Supply Chain / Audit (push) Waiting to run
|
||
|
|
c0d52334c5 | docs: update phase roadmap for v0.3.0 | ||
|
|
115e31945d | ci: fix clawhub skill publish command | ||
|
|
20fa631796
|
chore(main): release 0.3.0 (#61)
* chore(main): release 0.2.4 * chore: sync cargo lock for release 1.85 * docs: expand 0.2.4 release notes * chore: release 0.3.0 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Lahfir <nmhlahfir2@gmail.com> |
||
|
|
8fbf904997 |
feat: publish enhanced reliability release
Release marker for the enhanced reliability work merged in #54 so Release Please can parse a clean user-facing commit. |
||
|
|
0c0a5b8dbc
|
fix: harden agent-desktop reliability (#54)
* feat: add strict ref reliability core
* feat: add session-scoped reliability diagnostics
* fix: skip finder pseudo windows for snapshots
* fix: report wait and trace failure context
* fix: harden ref action reliability
* fix: close reliability review findings
* fix: harden reliability edge cases
* fix: close final reliability edge cases
* fix: address reliability follow-ups
* docs: update reliability docs and skills
* fix: harden wait and ref action reliability
* refactor: centralize reliability helpers
* fix: stabilize macos ref resolution
* refactor: organize binary crate modules
* fix: harden ref action reliability
* fix: harden ref action reliability
* docs: compound reliability patterns
* fix: harden ref reliability edge cases
* fix: harden source-window ref resolution
* fix: preserve safe window title fallback
* fix: make explicit snapshots session-independent
* fix: harden ref fallback resolution
* fix: fail closed on uncertain ref fallback
* chore: strip inline comments and enforce docstrings
Replace inline // comments with /// docstrings where they carry non-obvious
contract, and add a pre-commit guard so inline comments cannot regress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): preserve action results, delegate timeout resolution, tighten core boundaries
Apply verified code-review fixes: ref-action release failures no longer
mask successful action results (prevents double-dispatch on retry);
resolve_element_strict_with_timeout defaults to delegating so strict-only
adapters support wait --element; wait --text reports count only when
--count is requested; latest-refmap refresh logs load failures instead of
silently serving stale refs; InteractionPolicy moved to its own module and
actionability/trace modules scoped pub(crate) per file rules; duplicate
wait test helper extracted to shared support module; timeout error
constructors deduplicated; redaction test covers description; policy
focus-denial path covered; skills document steps array, actionability
details, trace redaction, and batch trace inheritance.
* fix(review): eliminate per-element AX round trips and close remaining review findings
Fold AXPosition/AXSize and the scrollbar probe into the existing
AXUIElementCopyMultipleAttributeValues batch so tree traversal and the
actionability preflight pay one IPC per element instead of up to four;
A/B benchmark shows strictly-faster snapshots with identical ref counts
and scroll capabilities (Finder 4.5s -> 2.4s same-session, Docker
Desktop parity at 440 refs / 933 scroll-capable nodes).
Consolidate the CLI and FFI ref-action pipelines into one core
execute_resolved path (actionability, tracing, and dispatch semantics
live once; FFI passes a default context). Remove the no-context
execute() shims from is/right-click/snapshot/wait and the test-only
helper shims; every command now takes an explicit CommandContext.
Split the macOS resolver into resolve (orchestration), resolve_search
(candidate collection), and resolve_classify (strict classification),
clearing the 400-LOC ceiling with room to grow.
Notification waits now retry transient baseline failures inside the
timeout budget with the same retryable gate as window/text waits and
report last_error in timeout details instead of aborting on the first
flake; a baseline is never fabricated.
Refmap writes clean up their temp file on failure, stale *.tmp orphans
are swept under the store lock, and save_existing_snapshot re-verifies
snapshot ownership inside the owning store's write lock with bounded
re-discovery before deterministically recreating in the caller's store.
Coverage hardening: zero-budget wait timeout shape, wait --text
--count 0 absence detection, ref-action pipeline call-count guard
(1 resolve / 1 live read / 1 dispatch), duplicate snapshot-id collision
on load, pruned-everywhere recreation, tmp sweep and rename-failure
cleanup, FFI AMBIGUOUS_TARGET last-error code assertion.
* docs: document notification retry, error-code contract, and diagnostics sensitivity
Note transient-error retry and last_error timeout detail on wait
--notification; make explicit that agents branch on error.code (message
and suggestion text is informational); warn FFI consumers that
ad_last_error_details may carry on-screen element names, values, and
window titles and should stay out of shared log surfaces.
* fix: validate find roles against the canonical vocabulary
find --role with a role no adapter can emit (textarea, typos) silently
returned ok with zero matches, reading as 'element absent' when the
query could never match. Role queries now resolve through a canonical
vocabulary in core: common text-input aliases (textarea, textbox,
searchfield) normalize to textfield case-insensitively, and unknown
roles fail with INVALID_ARGS carrying details.valid_roles so agents can
self-correct.
The macOS role mapping becomes a sorted single-source table with
binary-search lookup, and a conformance test pins every emitted role
(plus the unknown fallback and the synthesized cell role) to core's
CANONICAL_ROLES — the cross-platform contract Windows/Linux adapters
must map their native vocabularies into, enforced by the same
table + test pattern rather than convention.
* refactor: derive find role hints from the live tree, drop hardcoded vocabulary
Replace the canonical-role allow-list (and its hard INVALID_ARGS
rejection) with a tree-derived approach. A role filter that matches
nothing now returns ok with roles_present — the distinct roles actually
in the searched tree — so the caller distinguishes 'none on screen' from
a wrong role name and self-corrects. This needs no central role list: a
role any adapter newly emits surfaces in roles_present automatically,
with nothing to keep in sync across core and the platform crates.
A tiny role-query normalizer keeps the ergonomic win (textarea, textbox,
searchfield fold to textfield, case-insensitive) but never gates or
rejects — it is a synonym shim, not a vocabulary. The macOS role table
returns to its plain match form; the cross-crate canonical-vocabulary
list and its conformance test are gone.
* refactor: move protected-process knowledge out of core into the adapter
close-app hardcoded macOS/Unix process names (loginwindow, windowserver,
dock, launchd, finder) inside core, baking platform-specific knowledge
into the platform-agnostic crate. Windows would need csrss.exe/
winlogon.exe, Linux gnome-shell/Xorg. Add PlatformAdapter::
is_protected_process (default denies nothing); the macOS adapter owns its
list with substring matching over display and bundle identifiers. core's
close-app just asks the adapter. Also genericize a macOS-flavored test
fixture string so core carries zero native vocabulary even in tests.
Verified: core has no platform-native references in non-test source, no
cfg(target_os) gates; the only remaining cfg(unix)/libc use is securing
core's own refmap/trace/lock files with non-unix fallbacks.
* fix: stop close-app claiming a graceful quit it cannot confirm
close-app returned closed:true the instant a graceful quit was *sent*,
while the app was still running behind an unsaved-changes dialog —
a false completion claim. Empirically (NSWorkspace.runningApplications):
a clean quit completes in ~0.2s, a dialog-blocked quit never completes
on its own, and macOS confirms only that the quit request was sent, not
that the app terminated. Verifying by polling would add seconds of
latency on the exact (blocked) case it is meant to catch, so we do not
poll.
Graceful close now reports { method: graceful, requested: true } —
truthful and instant, no closed claim. --force is a synchronous SIGKILL,
so it reports { method: force, requested: true, closed: true }. Callers
needing graceful confirmation observe via list-apps / wait --window and
can drive a save dialog with snapshot + find, which is the agent-native
path.
* feat: add drag --drop-delay for reliable macOS drop registration
macOS drop targets need the dragged item to dwell over them before they
register as the destination; too short and the gesture lands as a drag
with no drop. The dwell was a hardcoded 500ms dead sleep. Expose it as
--drop-delay <ms> (CLI), drop_delay_ms (DragParams/AdDragParams, 0 =
adapter default sentinel matching duration_ms), and replace the dead
sleep with an event-driven dwell that posts LeftMouseDragged over the
destination every 16ms so the target stays highlighted instead of
dropping the drag mid-pause.
DRY: the C-to-core drag conversion (duration/drop-delay zero-sentinel)
was copied across three FFI sites; collapse them into AdDragParams::
to_core(). FFI ABI: AdDragParams gains drop_delay_ms (header + repr +
header-compile test). Tests: core threads the value into params and
response and omits the field when unset; FFI maps both optionals.
* fix: make action-bearing elements ref-able so scroll/expand can target them
E2E testing against a diverse fixture app surfaced that disclosure
(Expand/Collapse/Click) and scrollarea (Scroll) advertise actions but
never received refs — they are not in INTERACTIVE_ROLES — so the scroll,
expand, and collapse commands required a <REF> their own target roles
could never have. The commands were uninvokable against their primary
targets.
Ref allocation now gates on addressability, not role alone: an element
is ref-able if its role is interactive OR it advertises a primary action
(any action other than a bare SetFocus, which would ref-allocate inert
focusable containers). scrollarea and disclosure become ref-able;
scroll now works against a real app. Ref-count impact is modest
(fixture 61->72, Finder ~262).
Tests assert action-bearing containers get refs, SetFocus-only and inert
elements do not, and interactive roles stay ref-able without actions.
Contract docs (CLAUDE.md, SKILL.md) updated.
* fix: eliminate vacuous AX successes and harden resolution
Dogfooding the binary against a real fixture app surfaced five cases where a
command reported success without producing the effect, or failed with the wrong
error. Each is verified by independent before/after observation in the E2E
harness.
- is_menu_open no longer treats a latent AXMenuBar as an open menu, so select
and wait --menu-closed stop seeing a permanently-open menu.
- set-value coerces the written AXValue to the element's existing CFNumber/
CFBoolean/CFString type and verifies numerically, fixing sliders; steppers
converge via AXIncrement/AXDecrement when AXValue writes are vacuous.
- double-click only claims success when the element advertises AXOpen; otherwise
it fails closed instead of reporting a non-existent double-click.
- a completed resolution pass that proves a ref absent downgrades a deadline
TIMEOUT to STALE_REF so removed elements fail with the correct code.
- expand/collapse verify the disclosure state and fall back to a press-toggle
for press-driven disclosures; press-toggled containers expose EXPAND/COLLAPSE.
roles.rs adds the disclosure expandable role and normalizes textarea/textbox/
searchfield role queries to textfield.
* feat: add Playwright-style headed/headless interaction mode
Ref actions now run in exactly two modes. Headless is the default: semantic
accessibility operations only, no cursor movement, and a fail-closed
POLICY_DENIED when only a physical gesture would work. The global --headed flag
upgrades every ref action to permit focus stealing and cursor movement, so the
chain's physical click/double-click/scroll/keypress fallbacks can complete. The
AX path is always tried first, so --headed never regresses headless-capable
elements; it only adds fallbacks for elements that need a real gesture.
- CommandContext::request(action, base) builds the per-command request: each
command declares its headless base (pure-AX headless; type uses focus_fallback
because typing requires focus but never moves the cursor) and --headed upgrades
any base to the headed policy.
- The internal/FFI "physical" policy is renamed "headed" throughout, including
the C ABI enum (AD_POLICY_KIND_HEADED keeps discriminant 2) and bindings.
- Raw-input commands (press, hover, drag, mouse-*, key-down/up) are unchanged:
always physical, mode-independent low-level escape hatch.
- Unit tests assert every ref command is headless by default and headed under
--headed; docs (CONCEPTS, CLAUDE, skills) describe the two-mode contract.
* test: add E2E fixture app and dual-mode harness
Drives the release binary against a real SwiftUI/AppKit fixture and verifies
every effect by independent before/after observation — never the command's own
ok:true — so a command that reports success without an effect is caught. This is
the layer mock-adapter unit tests cannot cover: it exercises the contract
against the real macOS Accessibility API.
- AgentDeskFixture.swift exposes a fixed, diverse AX surface (native AppKit
slider/stepper, gesture-only and ambiguous controls, a sheet, a press-toggled
disclosure, async-appearing elements, a drag canvas). It is never tuned to
make a command pass; a failure is a finding about the CLI or the harness.
- run.sh drives every ref-action command in BOTH headless and --headed mode with
mode-specific target values, plus the double-click discriminator (headless
fails closed with POLICY_DENIED, --headed completes) that proves the two modes
differ. It also covers strict resolution, wait predicates, skeleton drill-down,
sessions, trace redaction, surfaces, drag, expand, and force-close.
- The compiled fixture .app is a build artifact (gitignored; built on demand).
Run: cargo build --release && bash tests/e2e/run.sh (needs AX permission).
* fix: close review runtime, correctness, and security gaps
Addresses validated findings from the branch code review:
- chain: thread the chain deadline into increment_to_value so a non-converging
stepper cannot spin up to 1024 AX round trips and blow past the timeout.
- mouse: a RAII guard posts LeftMouseUp if any fallible step of a drag returns
early, so an error can never leave the mouse button held down system-wide.
- ffi: cap caller-supplied state/action/path counts before from_raw_parts,
mirroring the existing MAX_MODIFIERS_PER_COMBO guard, to reject out-of-bounds
reads from a garbage C count.
- wait: the actionable predicate now forwards the structured ActionabilityReport
from error.details instead of dropping it to a flat message, so agents can see
which check is blocking.
- scroll: gate the row-select fallback on policy.allow_focus_steal so a headless
scroll can no longer silently change the user's table selection.
- actionability: delete the unreachable stable->StaleRef branch (stability_check
never fails by design) and its now-dead failed_check helper.
- status: delete two pub wrappers with no production callers; the test now drives
the real execute_with_report_with_context entry point.
* refactor: extract disclosure chain steps under the 400-LOC limit
chain_steps.rs had grown to 402 lines, over the hard per-file limit. The six
press-toggle disclosure helpers form a cohesive group and move cleanly into a
sibling chain_disclosure_steps.rs (following the chain_web_steps/
chain_menu_steps pattern); chain_defs.rs references the new module. No behavior
change.
* feat: focus the target window before ref-addressed physical input
Ref-action physical fallbacks (click/scroll/type) already brought the target
app frontmost before synthesizing CGEvents. The raw-input commands (drag, hover,
mouse-*) resolving a point from a ref did not — the resolver had the pid but
discarded it, so the adapter saw only coordinates and synthetic events could
land on whatever window happened to be frontmost.
Add a best-effort focus_app(pid) to PlatformAdapter (macOS uses
ensure_app_focused; other adapters default to not_supported). The point resolver
now focuses the ref's app before returning, so every physical interaction that
targets a known element raises its window first. Coordinate (--xy) input is
unchanged: the caller owns the target there.
* fix: add AdDragParams size guard and document the ABI breaks
AdDragParams gained a drop_delay_ms field but, unlike AdRefEntry, had no size
guard — an old caller's smaller allocation would let Rust read past it and turn
stack garbage into a real drop delay. Add AD_DRAG_PARAMS_SIZE, ad_drag_params_size(),
a compile-time layout assertion, and a zero-init note, matching the ref-entry
pattern.
This branch makes several consumer-visible contract changes that release
tooling must cut as a major. They are gathered here because the release workflow
ships the C header as an artifact.
BREAKING CHANGE: the C ABI and CLI/JSON contract changed on this branch.
- AdPolicyKind: AD_POLICY_KIND_PHYSICAL is renamed AD_POLICY_KIND_HEADED
(discriminant 2 unchanged, so compiled binaries are safe; source-level C
consumers must rename). No back-compat alias is kept — "physical" is gone.
- AdRefEntry grew (caller-allocated input); validate layout with
AD_REF_ENTRY_SIZE / ad_ref_entry_size().
- AdDragParams grew; validate with AD_DRAG_PARAMS_SIZE / ad_drag_params_size()
and zero-initialize before use.
- close-app graceful response no longer includes closed:true; it returns
{ method: "graceful", requested: true } because a graceful quit cannot be
synchronously confirmed.
* test: harden and expand the E2E proof layer
Closes the honesty gaps the review found and covers the interactions that were
missing, all verified by independent before/after observation:
- twins: the fixture twins now record distinct effects (twin-a/twin-b) and the
assert requires the ADDRESSED twin to fire (or AMBIGUOUS_TARGET), instead of
passing on any ok:true.
- click: click-status is a counter, so the headed pass must observe a fresh
increment rather than inheriting the headless pass's value.
- adds triple-click + hover (headed gestures), tab selection (TabView tabs are
radiobuttons), context-menu open + item selection, and menu-bar enumeration
via --surface menubar.
- adds a performance section reporting per-command CLI wall-clock (snapshot,
find, get, click, set-value, type) with a soft <2s snapshot gate.
- documents the SwiftUI CommandMenu and cross-app drop limitations as tracked
notes, not silent skips.
* docs: trim CLAUDE.md to standards and document gesture headless-capability
- CLAUDE.md: remove ~130 lines of reference material that duplicated code,
Cargo.toml, or the skills (full PlatformAdapter trait dump, Key Types listing,
macOS API listings, dependency/build-config tables, the 54-command table).
Replaced the stale trait dump with a pointer to adapter.rs (which also fixes
the review's stale-trait-docs finding) and kept only the non-obvious gotchas.
CLAUDE.md is now standards, invariants, and conventions.
- Document, on macOS (Phase 1), which gestures have a headless path: most ref
actions do; double-click via AXOpen; triple-click/hover/drag are cursor
gestures with no AX equivalent (physical only). The command surface is
platform-agnostic — a future Windows/Linux adapter that exposes a headless
path lights it up with no command or core change. Added to README and the
interaction reference.
- FFI skill: AD_POLICY_KIND_PHYSICAL is now AD_POLICY_KIND_HEADED.
* docs: capture gesture headless-capability learning and refresh policy docs
- Add best-practices/macos-gesture-headless-capability: which desktop gestures
have a headless AX path on macOS (double-click via AXOpen; triple-click/hover/
drag are physical-only; SwiftUI controls vs native AppKit), and why the command
never decides — the platform adapter owns headless-vs-physical.
- Refresh two policy learnings for the physical->headed rename: ActionRequest::
physical -> headed and AD_POLICY_KIND_PHYSICAL -> AD_POLICY_KIND_HEADED, noting
the new global --headed upgrade path via CommandContext::request.
* fix: address review P2 correctness and reliability findings
- wait: cap each ref-resolution attempt (750ms) so a slow resolve cannot
consume the whole wait budget on the first poll; the predicate is re-checked
across the full timeout.
- wait: make LatestRefCache timing fields private (no external readers).
- wait: add a unit test for --menu-closed (asserts it waits for open=false).
- close-app: make the graceful and force responses symmetric — both carry
`closed` (force confirms true; graceful cannot confirm, so false) instead of
graceful silently omitting the field.
- close-app: test adapter-error propagation.
- adapter: the default resolve_element_strict_with_timeout now logs that it does
not enforce the deadline, so an adapter that forgets to override it is visible
in traces.
- type: a RAII guard restores the user's clipboard on every scope exit (success,
error, panic) during the paste-based non-ASCII path, shrinking the clobber
window to an unpreventable SIGKILL.
Note: the reviewer's "fail fast on AMBIGUOUS_TARGET with a pinned snapshot"
suggestion is not applied — existing tests prove transient ambiguity resolves on
retry even with a pinned ref (ambiguity is a property of the live tree, not the
refmap), so failing fast would regress intentional, tested behavior.
* perf: trim actionability preflight and resolve-search allocations
- action_list: gate the AXValue and AXExpanded `is_settable` probes on whether
the role could plausibly carry that capability (unknown roles always probe),
skipping up to two AX round trips per preflight on common click-only targets.
No capability is lost — value/expandable roles still probe.
- resolve_search: reuse one scratch FxHashSet across nodes instead of allocating
a fresh dedup set per node during path and recursive search.
* refactor: split actionability types into one file each
ActionabilityStatus, ActionabilityCheck, and ActionabilityReport move into their
own files under a new actionability/ module (following the tree/ and actions/
folder pattern); mod.rs keeps the check logic and re-exports the types. Honors
the one-domain-type-per-file rule without changing behavior.
* test: split fixture under the LOC limit and fix swift-ios issues
- Extract the reusable fixture components (status readout, native AppKit
slider/stepper, drag canvas, card) into FixtureComponents.swift so each file
is under the 400-LOC limit; build.sh now compiles every .swift in the dir.
- NativeSlider/NativeStepper implement updateNSView so a SwiftUI binding change
syncs back to the NSView.
- The fixture no longer steals focus unconditionally on launch
(activate ignoringOtherApps:false), so it cannot mask headless-policy focus
violations; the harness drives focus explicitly.
- build.sh pins the SDK and a macOS 13 deployment target for reproducible builds.
* docs: document the optional error.details field and roles_present shape
- SKILL.md: note that the error object may carry an optional `details` (the
actionability report, AMBIGUOUS_TARGET candidates, or a wait TIMEOUT's last
observed state) and that responses should be parsed leniently — `details` and
future fields are additive.
- commands-observation: show the no-match `find` response with the
`roles_present` hint so callers can tell a wrong role name from "none on
screen".
* fix: gate the ref-gesture focus raise on the interaction policy
Ref-addressed hover/drag raised the target app unconditionally, violating
the headless no-implicit-focus-steal contract. The point resolver now
returns the owning pid instead of focusing, and commands decide: headless
never raises, --headed raises once (drag focuses only the from-app, fixing
the cross-app double-focus). Responses report focused:true so multi-app
agents can detect the frontmost change.
* fix: abort failed drags at the origin and disarm the guard only on success
The mouse-up guard disarmed before the final fallible up-event, so a
failed final post left the button held. Worse, its corrective release
fired at the unreached destination, silently committing an aborted drag
as a completed drop (CGEvents resolve at their embedded coordinates).
The guard now owns the release: it disarms only after the up actually
posts, and an early return cancels by dragging back to the origin and
releasing there.
* fix: enforce the chain deadline inside increment steps
All dispatch sites construct ChainContext with deadline: None, so the
remediation parameter on increment_to_value never received a value and
the 1024-iteration loop ran unbounded by the chain timeout. The chain now
pins its resolved deadline into the context every step observes. Also
extracts the pure write-verification predicates and their tests to
chain_verify.rs, bringing chain.rs back under the 400-LOC limit.
* fix: pin the AdAction ABI layout and bound FFI string and array inputs
AdDragParams is embedded by value in AdAction, so its 8-byte growth grew
the struct C callers pass to ad_execute_action with no size guard —
old-layout callers under-allocate and stack garbage becomes a live
drop_delay_ms. Adds AD_ACTION_SIZE / ad_action_size() with a layout pin,
matching the AdRefEntry pattern.
Also hardens the input boundary: C strings are decoded with a bounded
NUL scan (AD_MAX_STRING_BYTES, sized for CLI argv parity) so a missing
terminator cannot walk arbitrary memory, and the single coarse 1024
array cap becomes published per-field caps (AD_MAX_REF_STATES/ACTIONS/
PATH_DEPTH) with tests just over each limit.
* perf: replace fixed input settle sleeps with state polls
ensure_app_focused slept 50ms per physical input even when the app was
already frontmost; it now polls AXFrontmost (1ms, 50ms deadline).
disclosure_settled slept an unconditional 40ms up to three times per
expand/collapse; it now polls the disclosed state (5ms, 200ms deadline),
converging immediately on fast UIs. The kAXFocusedAttribute settability
probe is gated by role_may_accept_focus, mirroring role_may_bear_value,
and key dispatch reuses ensure_app_focused instead of its inline
duplicate.
* feat: check a specific action in wait --predicate actionable
The actionable predicate hardcoded Click, so wait-then-type flows got a
false ready on fields that cannot accept text (the editability check only
runs for editing actions). --action selects click (default), type,
set-value, or clear, and the preflight mirrors each command's real base
policy (type uses its focus-fallback base).
* fix: redact title, url, help, and placeholder keys in traces
Window titles, URLs, tooltips, and placeholder text carry user content
just like names and values; the redaction list now covers them.
* test: harden the e2e harness and unify fixture AX labels
The harness now fails setup loudly when the fixture build or AX trust is
missing, rebuilds the fixture when sources are newer than the bundle,
asserts hover only against a freshly observed state, and force-collapses
the disclosure so the expand test proves a real flip. The slider/stepper
labels live solely on the NSViews (the AX-actionable elements), removing
the macOS-version-dependent race between two label sources, and the drag
canvas reports a zero frame when detached from a window.
* docs: record the perf commit type, pre-1.0 bump policy, and error details field
Adds perf: to the allowed commit types (release-please already maps it to
a Performance changelog section), records the pre-1.0 versioning policy so
a BREAKING footer is expected to cut a minor rather than a major, and
shows the optional error.details object in the error envelope docs.
The gitignored local AGENTS.md mirror got the same contract sync.
* fix: surface increment deadline expiry as timeout with the observed value
A chain deadline firing mid-increment returned Ok(false), so the step was
recorded as skipped, the chain exhausted into ACTION_FAILED, and the
control sat at a half-applied value the caller could not see — post-state
is only read on success, and ACTION_FAILED recovery guidance points away
from retrying. Expiry is now a TIMEOUT error carrying value_before,
value_at_timeout, target, and a mutated flag in details.
* perf: cap the disclosure settle poll to the chain deadline and widen its interval
The settle poll could spend 3 x 200ms x 5ms-interval reads (~360 IPCs)
per expand/collapse and overshoot the chain's own deadline. A new
CustomWithDeadline chain step threads the chain deadline into the
disclosure steps, the settle budget is min(200ms, remaining chain
budget), and the interval widens to 20ms (~30 IPCs worst case).
* fix: enforce the protected-process guard inside the adapter close path
The guard lived only in the CLI command layer, so ad_close_app could
force-kill session-critical processes (loginwindow, WindowServer, Dock)
that the CLI refuses. close_app_impl now refuses them before any side
effect with the exact CLI error contract, making CLI, FFI, and any future
consumer behave identically; the CLI preflight remains as an earlier
check against the same predicate.
* fix: make focused semantics honest and confirm window focus by polling
ensure_app_focused set AXFrontmost unconditionally and reported success
identically whether or not a raise happened; it now no-ops when the app
is already frontmost, so Ok (and the focused:true response field) means
"frontmost ensured" exactly as documented. focus_window_impl gains the
same confirmation poll after its raise, and the poll interval widens to
5ms (10 reads max in the 50ms window).
* fix: attach abort-state guidance to drag failures and document cancel limits
Drag synthesis errors surfaced as bare INTERNAL with no hint about the
gesture's end state. Failures now carry a suggestion stating the button
was released back at the origin (best-effort), no drop was committed,
and where the cursor ends; the guard doc spells out the two best-effort
limits (corrective posts can fail; a self-drop at the origin is a no-op
for most targets).
* refactor: split oversized files by responsibility under the 400-LOC limit
helpers_tests (426) splits into resolution/window/pipeline tests, a
ref-action+trace test file, and a shared entry-builder support module.
wait_element_tests (414) splits into predicate-behavior tests and
resolution/lifecycle tests over a widened wait_test_support. wait.rs
(395) loses the element-wait loop to wait_element.rs, and refs_store
(397) moves its tmp-cleanup/retention methods to a refs_store_prune
child module (declared via #[path] so the split keeps base_dir and
snapshots_dir private to the store).
* refactor: build the actionable preflight request per action name at parse
The policy mirror lived in a separate helper with a catch-all arm, so a
future action name could silently inherit the headless policy. Parse now
maps every --action name to the exact ActionRequest its real command
runs (type is the only focus-fallback), the catch-all is gone, and a
test pins each name's policy.
* refactor: move point resolution and the focus helper to point_resolve
PointResolveArgs, ResolvedPoint, the ref-or-xy resolver, and
focus_for_physical_input were accumulating in helpers.rs alongside
unrelated ref-action plumbing; they now live in a dedicated
point_resolve module consumed by hover and drag.
* test: keep the drag canvas AX label on the NSView only
The DragCanvas carried two label sources (the NSView and a SwiftUI
modifier on its representable), the same macOS-version-dependent race
the slider/stepper fix removed; the harness-facing label now lives
solely on the AX-actionable NSView.
* docs: sync skills and header with the focused, redaction, and wait contracts
Documents the ensured (best-effort, already-frontmost-aware) semantics of
focused:true and its absence-vs-false meaning, the four redaction keys
added in round 2 plus the substring-match behavior, the INTERNAL error
recovery row, the FFI wait-surface asymmetry, the cross-app drag
occlusion caveat, and the AdDragParams/AdAction layout history with the
adjudicated pre-1.0 breaks so fresh reviews stop re-finding them.
* fix: surface settle-wait deadline truncation as timeout with a schema discriminant
A chain deadline truncating the disclosure settle wait returned a plain
step failure, exhausting into ACTION_FAILED — the same masking class
fixed for increments — even though the triggering action may still land
after the truncated wait. Settle exits are now classified: full-budget
misses stay step failures, deadline-truncated waits raise TIMEOUT with
the wanted/observed state, and the poll sleeps are clamped so a tight
deadline still gets at least one read. All TIMEOUT details now carry a
kind discriminant (wait_timeout vs chain_deadline) so agents can branch
without sniffing field names.
* fix: match protected processes exactly, not by substring
'docker'.contains('dock') permanently blocked close-app for Docker,
FinderSync-class apps, and anything else embedding a protected name.
Matching is now an exact lowercase name or an exact dot-separated
bundle-id component, so Dock and com.apple.dock stay protected while
Docker, Docker Desktop, FinderSync, and PathFinder stay closable —
pinned by false-positive tests.
* fix: raise the element's window before the physical click fallback
CGEvents land on the topmost window at the click point, so an app being
frontmost is not enough when the target element lives in a background
window of that app — the physical fallback clicked whatever overlapped
it, and the skip-raise-when-frontmost optimization widened the window
for that. click_via_bounds now raises the element's own AXWindow (AXRaise,
AXMain fallback, brief confirmation poll) via a shared window_ops helper
that focus_window_impl reuses. Verified live: a headed click on a ref in
an occluded Finder window raises that window and lands the click in it.
* test: guard ref-action policy coverage against silent gaps
A new ref-action command could ship without a base-policy assertion. A
guard test now scans crates/core/src/commands/ for files calling
context.request( and fails unless each stem appears in the
POLICY_TESTED_COMMANDS list backing the policy assertions.
* fix: carry the protected-process suggestion on the CLI preflight
The CLI-layer guard returned bare INVALID_ARGS while the adapter layer
carried recovery guidance, so agents on the primary surface got 'check
command syntax' for a permanently-disallowed operation and looped on
argument fixes. Both layers now state the same suggestion.
* docs: document the TIMEOUT schemas, chain deadline knob, and roles_present scope
Names the two TIMEOUT details schemas by their kind discriminant with
the mutated-flag retry rule, points chain-deadline recovery at
AGENT_DESKTOP_CHAIN_TIMEOUT_MS instead of --timeout, extends the
roles_present hint to all non-count selection-mode misses, and aligns
the STALE_REF recovery row with the richer error.rs suggestion.
* test: extract the scroll card and document drag-canvas data flow
AgentDeskFixture.swift sat at exactly 400 lines; the scroll card is
fully self-contained (its offset state never leaves the card), so it
moves to FixtureCards.swift as a standalone view with zero bindings,
landing the main file at 374. Harness-facing labels are byte-identical.
DragCanvas gains two intent comments distinguishing the deliberately
empty updateNSView from a forgotten sync.
* chore: pin ABI sizes for C consumers and explain the prune module split
C11-gated _Static_asserts mirror the Rust-side layout pins so a C
consumer compiling against a drifted header fails at build time, and
the production #[path] prune module carries its privacy rationale.
* test: give racing wait tests a deterministic budget
Three wait tests used a 1ms timeout that can elapse before the loop's
first resolution attempt under load, flaking on machine pressure (the
ambiguous-resolution test needs at least one attempt to record its
observation). 50ms guarantees the first attempt without slowing the
suite.
* docs: capture three round-4 review learnings and grow the concept map
Documents the abort-state contract for multi-step physical input (guard
disarm ordering, origin release, end-state suggestions), the three-layer
repr(C) size-pinning discipline born from the AdAction silent-growth
incident, and the named-arms-plus-exhaustiveness-guard pattern for
policy/dispatch mirrors. CONCEPTS.md gains Action Chain and Protected
Process and refreshes Coordinate Fallback with the window-topmost rule.
* docs: refresh six learnings against the enhanced-reliability branch
Brings the learning corpus back in line with code that moved this
branch: the gesture-capability and policy docs now describe the
window-level raise in the physical path and link the new abort-state
doc, the reliability contract documents the TIMEOUT kind discriminant
and the wait --action per-name policy variant, the FFI review rule
covers structural repr(C) size drift alongside behavioral parity, the
allocator doc records how the config struct absorbed four more fields
in one place, and the fingerprint doc names the real tri-state decode
error type. Three docs verified accurate with no edits.
* docs: sync roadmap with current reliability contracts
* fix: harden desktop action reliability
* fix: harden reliability review regressions
* fix: close final reliability review gaps
* fix: close reliability review gaps
* test: avoid raw pointer mutation in ffi free tests
* refactor: trim reliability branch dead code
* fix: close reliability review gaps
BREAKING CHANGE: the C ABI AdActionResult layout now includes action steps; C consumers must rebuild against the updated agent_desktop.h header.
* fix: close reliability review gaps
* ci: scope cache hash inputs
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
1aa4f5f10f
|
chore(deps): bump smallvec to 1.15.2
Some checks failed
CI / Format (push) Has been cancelled
CI / Test (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
Release / Release Please (push) Has been cancelled
Supply Chain / Audit (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (aarch64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-apple-darwin) (push) Has been cancelled
Release / Build FFI (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build FFI (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / Publish to GitHub Release (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish Skills to ClawHub (push) Has been cancelled
|