feat: redesign full tweak system - #32
Open
ehsan18t wants to merge 52 commits into
Open
Conversation
First-principles design for the effect/apply/snapshot layer: one typed Effect model, per-tweak elevation floor with per-step escalation and TI acquire-vs-operate handling, ephemeral actions, hosts/firewall kept as typed kinds, build-number OS scoping, and a snapshot-history / Restore-Snapshot model. Rewrite ADR-0003 (System Default is a computed status, not a revert target), amend ADR-0002 (snapshot history + checkable-only compare), add ADR-0005 (elevation is declared per tweak, never silently escalated). Remove the superseded TWEAK_SYSTEM_REVIEW.md.
Expand the §6 example to show an HKCU (floor carve-out) effect and a typed Hosts setting, and fix the §4 diagram (restore, hosts/firewall kinds). Reconcile snapshot/restore wording across the spec after the redesign: history of return-points (not a single baseline), Restore Snapshot vs revert, per-option detectability, and System Default as a status rather than a selectable target. Rename the ADR-0003 file to match its rewritten 'computed status' conclusion.
…lementation plan Fold the full design-review and grilling outcomes into the binding docs: presence semantics, shared claim/release ownership, restore-as-reapply, .reg-aligned authoring surface, merged registry paths, the redesigned Windows-version model, Unknown/Unavailable statuses, and the WAL action journal. - spec rev 2 with 26 normative invariants and a resolved-decision log - ADR-0002/0003/0004/0005 amended; ADR-0006 (one address, one owner) and ADR-0007 (option snapshots are references) added - CONTEXT.md rebuilt on the rev-2 vocabulary - implementation plan for the engine rebuild and hard cutover
…ess, residue rule)
ehsan18t
force-pushed
the
tweak-system-redesign
branch
2 times, most recently
from
July 22, 2026 17:39
1723ae1 to
d19efc0
Compare
Implements ActionKind (run_apply/run_undo/run_probe) for the imperative Action escape hatch: cmd/powershell scripts via -EncodedCommand (Cmd via a temp script file, since cmd.exe has no encoded-command flag) plus DeleteTree reusing the hardened registry key-delete through RegistryKind::drive. Exit 0 is success/present; non-zero is a typed ActionFailed(code); a script that fails to spawn or exceeds the 30s hand-rolled timeout is ActionExecFailed, never a benign Ok. No-undo and no-probe calls return Error::Invalid rather than a silently misleading success/false. Adds the missing build guard rejecting an ephemeral action that also declares undo/probe, with a fixture pinning it.
…valid classification
Implements the apply pipeline and ADR-0001 rollback (spec §8.1): capture pre-apply state before any mutation, persist the WAL journal before driving a single effect, drive every effect in declaration order with did-it-work verification, and on any failure undo completed actions in reverse plus drive the captured state back -- a verified full rollback consumes the snapshot entry, an incomplete one keeps it as Needs Attention. Adds the per-tweak async lock and crash-residue scan (lifecycle.rs), and a new ActionRunner seam (engine/mod.rs) so Action apply/undo can be mocked in tests exactly like the existing ProbeSource seam, keeping the whole suite at zero OS contact.
…ller, winver runtime - winver.rs: real running Windows version via RtlGetVersion + UBR; WindowsScope::applies implements the full products/build/revision grammar for runtime scope decisions, while validate.rs's Milestone-based build-time guards stay untouched. - engine/context.rs: effective_level (max, escalate-only), route (HKCU always in-process as the interactive user), group_steps (consecutive System/TI batched, User/Admin never grouped), and the over-the-shoulder SID guard behind an injectable probe. - broker.rs: run_ops adds a multi-op caller beside run_one/run_elevated_broker (unchanged wire protocol), distinguishing couldn't-acquire-the-child from acquired-but-op-failed. - engine/mod.rs (AllKinds::drive): routes System/TI drives through the broker via a mechanical Setting -> BrokerOp translation per kind (registry/service/task); Hosts/ Firewall and field-addressed registry writes remain unrouted (UnsupportedLevel). Engine- level grouping of consecutive effects is not yet wired -- group_steps is a primitive for a follow-on task. - kinds/mod.rs: new CouldNotAcquireElevation error variant, distinct from the existing AccessDenied (now reused for "acquired but denied"). Manually verified one grouped System-level batch (RegSet -> RegDeleteValue -> RegDeleteKey) end-to-end against the built magicx-toolbox.exe --broker mode while elevated; confirmed via an external PowerShell Test-Path that no residue remained afterward. See .superpowers/sdd/task-14-report.md for the full transcript and the cargo-test-harness limitation that prevented shipping this as a passing #[ignore]d test.
…ine and corpus Author examples.yaml (8 tweaks: registry tri-state absent, service+task with optional/if_missing, hosts+firewall, action with probe+undo+ephemeral, a shared claim/release pair, a packed-value field, and a windows-scoped tweak), rewire build.rs to load+validate it via the new model/parse/ validate/schema pipeline and embed the compiled corpus, and prove the engine end-to-end against real HKCU state in tweaks::e2e_tests. E2E testing surfaced a real gap in restore's Values-dump path: it never released a Shared claim the reverting tweak held (only the OptionRef path drove Shared effects), permanently stranding a shared value away from its captured original on the common "first apply from a virgin machine claims a shared setting" path. Fixed in engine::revert. Hard cut, no dual pipeline: deleted the old option-centric schema (models/tweak_schema.rs, models/tweak.rs, models/inspection.rs, models/tweak_snapshot.rs), services/backup/, the old tweak/backup Tauri commands, services/tweak_loader.rs, and all 9 old tweak YAML files. The low-level Windows types those primitives still need (RegistryHive, RegistryValueType, ServiceStartupType, SchedulerAction, HostsChange, FirewallChange, and siblings) were extracted verbatim to models/win_types.rs first. Orphaned elevation/service helpers whose only callers were the deleted pipeline were removed to keep the gate clean. The tweak Tauri command surface is intentionally empty until Task 16.
ehsan18t
force-pushed
the
tweak-system-redesign
branch
from
July 23, 2026 05:48
d19efc0 to
b7b6ef8
Compare
Adds the thin Tauri command layer over the redesigned tweak engine: get_tweaks, get_statuses_stream (background-progressive scan emitting one tweak-status event per tweak), apply_tweak, restore_tweak, list_snapshot_entries, discard_snapshot_entry, rescan_after_elevation, and get_elevation_state. SnapshotStore/ClaimsStore/ProbeCache are managed once via Tauri state and shared across every call; apply_tweak/restore_tweak refuse (typed error) a tweak whose elevation floor or SID state disables it, before ever reaching the engine. Wires the crash-residue scan at startup and re-prefixes a restore failure's wording at the command boundary rather than in the engine.
Rewrite the API layer to the 8 Task-16 commands + the tweak-status event, and adapt the tweak stores to fill statuses in incrementally as events arrive. Apply is by option label; restore is a single head-walk; discard replaces keep-current- state. Batch flows become client-side loops (no backend batch command). The card and details modal render the new states (Unknown/Unavailable/Residue/held-by/ Needs-Attention, availability + SID-mismatch) with existing primitives; 1 option renders a Switch, >=2 a Select. Categories are derived from the model. Profiles stay disabled (already neutralized). No src-tauri changes.
…chitecture to the new engine
Author-facing and end-user documentation uses commas, colons, parentheses, semicolons, or separate sentences instead of em dashes.
The over-the-shoulder guard read the console session's user via WTSQueryUserToken, which requires SE_TCB_NAME. Only LocalSystem holds that privilege, so the call returned ERROR_PRIVILEGE_NOT_HELD on every real run, elevated or not. The guard's fail-closed arm read that guaranteed failure as a mismatch and refused all 54 elevation: user tweaks. Measured at both privilege levels before and after. Three faults, fixed together: Probe. ProcessIdToSessionId -> WTSQuerySessionInformationW -> LookupAccountNameW needs no privilege. It also asks about the process's own session rather than the console session, since under RDP those legitimately belong to different users and comparing against the console reports a mismatch between accounts that were never in conflict. When name resolution fails (domain-joined with an unreachable DC, or Entra), it falls back to comparing SAM account names, which need no directory; names are only ever compared with names, so a half-resolved state degrades to "unknown" instead of accusing another account. Keying. The guard consulted the tweak's elevation floor while routing consulted the hive. The floor states which privilege a tweak needs, the hive states whose state it changes, and 31 admin-floor tweaks in the corpus drive HKCU effects that went unguarded while the user-floor ones were blocked wholesale. Both now go through one corpus-aware effect_is_hkcu, which also resolves shared blocks and DeleteTree actions; a corpus-wide test asserts the two agree for every tweak. Honesty. An unreadable SID still blocks, but reports a distinct SidUnknown state instead of claiming another administrator elevated the app. Fail-closed is deliberate: refusing mutates nothing, while a wrong-account write has no return point, because the snapshot store is keyed to the machine and apply verifies a write by reading back the same hive it just wrote. Also replaces is_running_as_admin's HKLM KEY_WRITE probe, which collapsed every error kind through .is_ok(), with GetTokenInformation(TokenElevation), and rejects HKCU addresses in to_broker_op so the routing convention becomes a typed guarantee. Reasoning and the rejected fail-open alternative: ADR-0005 amendment and docs/plans/fix-hkcu-user-level-gate.md.
Replaces the demonstration corpus with eight real category files, built from an independent four-pass validation of every tweak against primary sources. 203 tweaks in, 255 out: 10 deleted, 62 added, 166 corrected. A new `ai` category collects the Recall, Click to Do, Copilot and in-box AI controls that were scattered across privacy, interface and debloat. Twelve tweaks did not work as authored. ssd_optimize_trim wrote a value name Windows does not read; ultimate_performance_power_plan chased a GUID that /duplicatescheme had already replaced; asr_block_lsass_theft carried a GUID one character off Microsoft's, which Defender silently ignores; enable_credential_guard wrote to the state mirror instead of the input key. Three reverts were worse than never applying the tweak. Reverting disable_online_speech_recognition fabricated a cloud-speech consent the user never gave, and reverting require_smb_signing disabled a protection 24H2 enables by default. Both now restore the real stock state. Every tweak carries an info block in one shape: hook, what it does, benefits, drawbacks, good to know, a recommendation that takes a position, and an evidence block naming the risk level, whether the mechanism is Microsoft-documented or community-corroborated, and its sources. Names and option labels say what the control does. A tweak with one or two options renders as a segmented switch with a tick, a cross and no text, so the name is the only thing telling the user which way the tick goes: all 229 such tweaks start with a verb naming what their first option does, and the 26 with three or more keep noun names because their dropdown spells out every option. No option is labelled the Windows default. What Microsoft ships today is a fact about this month's Windows, not about the control, and pinning it into a label means an edit every time Microsoft changes its mind. An option that writes `absent` is named for where absence actually lands the machine, so deleting the WDigest policy reads as Enabled rather than "not configured". Three labels read backwards and are fixed. disable_lmhash_storage said "Enabled", meaning the prevention was on, which beside its title read as "LM hash storage: Enabled". filter_admin_token said "Enabled" while its subject is the built-in Administrator account. remote_uac_token_filter offered "Not filtered" writing `absent`, but absent behaves exactly as 0, so both options left the token filtered and the label named a state it could not reach. Four addresses were claimed by two tweaks each, visible only once all eight files compiled together. In every case the dedicated tweak keeps the address and the bundle drops it: disable_ui_animations over optimize_visual_effects, disable_wmp_network_sharing over disable_ssdp_upnp, task_autochk_proxy over disable_ceip_tasks. Research and sources: docs/superpowers/research/validation/
Every source, correction and verdict behind the corpus rebuild, one document per category plus the cross-cutting analyses. Four passes: a first validation round, an adversarial round that re-attacked the 87 entries the first round had called clean and flipped 27 of them, a gap hunt that proposed 71 controls the corpus lacked, and a rewrite of every tweak's user-facing copy into one template. The evidence rule is in _SPEC.md and is deliberately not Microsoft-only. Microsoft documents what it wants administrators to configure, not everything the OS reads, so treating "no Microsoft page" as "doubtful" penalises working tweaks. Three or more genuinely independent community sources agreeing on key, value name, type and semantics also verify, labelled so the reader knows which kind of confidence they are getting. Under that rule all 14 UNVERIFIED verdicts from the first pass resolved. _SPEC.md also carries the rule the option labels follow: an option is a state the app drives the machine to, so every literal in one has to be established, and no option is ever labelled the Windows or stock default. _harmful-revert.md carries the class that outranks the rest: a wrong mechanism makes a tweak inert and the user investigates, while a wrong revert does damage at the moment the user is trying to be careful. A clean 24H2 image baseline is the one missing input, tracked in the README's open questions.
…weak info
Three things the new corpus needs from the frontend.
Categories now come from the corpus. Registers the get_categories command and
its binding, so the sidebar reads id, name, icon and description from the
compiled corpus instead of a hardcoded list, and adding a category file is the
only step needed to make it appear. The icons the eight files name are
registered, including robot-outline for the new AI category. Corpus category
names are longer than the old hardcoded labels, so the sidebar label truncates
rather than forcing the row wider, and the count badge no longer shrinks for it.
Info blocks now end with an evidence section citing two to four sources as
markdown links, which the renderer emitted as literal bracket-and-paren text.
Links are extracted before the emphasis passes and shielded behind a sentinel so
an underscore inside a URL can never become an <em>. Only http and https become
anchors; any other scheme renders as plain text, because an href the webview
navigates natively is a script-execution path and this is a shared component
that may one day render something less trusted than the corpus. The body is
rendered with {@html}, so no component can be embedded per anchor; one delegated
click handler routes anchor clicks through the Tauri opener, matching
ExternalLink.
Also rewrites the profile export note, which ran two clauses together across an
em dash that the project style forbids.
… when it is the state The card treated two authored options as a toggle and picked the OFF side by matching "(Stock Default)" in a label. With that convention gone the regex matched nothing, and a dropdown for a two-way choice was the wrong control anyway. SegmentedSwitch already existed for this and had no callers; it now renders every tweak with one or two authored options, and only three or more fall through to the dropdown. System Default was also offered on every tweak as a permanent extra position. It is a computed status, not a state anyone picks: it means the live surface matched none of the authored options, so it only exists while that is true. It now appears, selected, exactly when nothing matches and disappears once an option does. The way back out of an applied option is Restore, which is its own button. SegmentedSwitch gains per-segment disabled so an option the machine cannot satisfy is shown and dimmed rather than hidden, with arrow-key navigation skipping it.
System Default was the one status that told the user nothing. Active names an option they can go read and Unknown lists the effects it could not read, but System Default said only that the machine matches none of the options, with no way to see what it does match. detect already reads the live value of every applicable effect into a map to do the matching, then discarded it. That map is now returned as `observed`, and the command layer pushes each reading through `push_setting_change`, the very same builder the authored options use. The result is an option-shaped block labelled "Your system right now", rendered by a snippet extracted from the option markup, so the system state and the options it failed to match are drawn by identical components with identical addresses and formatting. The user compares like with like instead of decoding a value dump. Beneath it sits the agreement list: which option each individual setting agrees with. A surface split across options is the usual reason nothing matched, and this names exactly where the machine straddles. Populated for SystemDefault only, so the per-tweak status event for the rest of the corpus is unchanged and the panel's whole render guard is its presence. Building it needs the tweak, which `From<TweakStatus>` does not have, so the conversion leaves it `None` and `scan_and_emit` fills it in. The status view and its two embedders drop `Eq` because the change views carry `serde_json::Value`, which is `PartialEq` but not `Eq`. The restore path leaves it empty on purpose; the next detect fills it in.
Four things the corpus rebuild made stale or wrong. CONTEXT.md told authors to write option labels like "500 KB (Stock Default)", which is where the corpus convention came from. Stock Default stays as a term for research and info prose, where explaining what Windows does is the point, and is now barred from option labels: it encodes what Microsoft ships this month into data that outlives the decision. The Option entry states the rule the labels follow, that an option is a state we drive the machine to, so every literal in one must be sourced and where a state cannot be established the tweak authors fewer options rather than inventing one. ARCHITECTURE.md, TWEAK_AUTHORING.md and TWEAK_SYSTEM.md still described the redesign's demonstration corpus as what ships, with ten references to an examples.yaml that no longer exists. The worked examples in the authoring guide stay as they are, since they were always self-contained illustrations checked against the validator rather than excerpts; the guide now says so and notes that their MagicXToolboxExample addresses are deliberate placeholders. KNOWN_ISSUES.md records the defects we have accepted for now, as bugs to fix rather than settled behaviour. PRE_MERGE_TASKS.md tracks what must land before this branch merges, including the shared-address multi-value analysis kept as reference so the deferral does not have to be re-derived. The file removes itself as part of the merge.
ehsan18t
force-pushed
the
tweak-system-redesign
branch
from
July 27, 2026 04:57
748527f to
8eb7bc6
Compare
The backend detect scan began emitting tweak-status events before the frontend had built its tweak list, so the earliest events were dropped silently by setStatusView and loadModel then reset every status to a fresh loading placeholder. Nothing re-sent them, leaving the first few tweaks in corpus order stuck on Checking for the life of the session. Await initializeQuick() before starting the stream (Svelte mounts +page before +layout, so loadRemainingData kicked the scan first), and buffer any status view that still arrives ahead of its tweak so loadModel seeds from it instead of overwriting it.
The per-effect agreement rows under "Your system right now" printed the author's YAML effect id (box_suggestions, bing_enabled), an internal handle that lets an option set a value without repeating its address. It means nothing to someone reading the change list directly above it. Carry the concrete address on the view instead: the registry value name, service name, task name, hosts domain or firewall rule name, matching how the change list already names each one. Paths are dropped, since the row above carries the full address and the short name is what tells two rows apart. The effect id stays on the view as the render key, which display names cannot serve because they are not unique within a tweak.
…y SYSTEM path `SvcStart`, `SvcStop`, `Powershell`, and `RawCmd` had no producer: no `to_broker_op`/`to_broker_ops` translation ever emitted them. They stayed live in the request deserializer, so anything able to hand the elevated child a request file could reach two interpreters (`powershell -EncodedCommand` and `cmd /c`) running as SYSTEM or TrustedInstaller. Removing them also removes the broker's duplicate base64 encoder, which action.rs already flagged as a carry-forward. Dropping the two service ops left `service_control::start_service`/`stop_service` and their poll/dependent-enumeration helpers with no callers. The tweak schema drives a service to a startup type and never starts or stops one, so those go too; git keeps them if a future effect kind needs them back. The single-op path went the same way. `write_registry_json_value`'s `use_system` branch was unreachable (its only caller, the broker, hardcodes false), which made `set_registry_value_as_system`, `run_one`, and `into_single` dead. `run_ops` is now the module's only entry point. Also drops the `trusted_installer` alias that made call sites name a module that no longer exists.
…ing as success `run_ops` scanned `results: Vec<OpOutcome>` for an `Err` and returned `Ok(())` if it found none. It never checked the length, so a response carrying fewer results than ops were sent, with none of them an error, reported the whole batch as applied. The executor never produces that shape, but nothing in the protocol said it could not, and the engine's read-back verify does not cover it: driving a service to Disabled sends `SvcSetStartup` plus a `DelayedAutostart` companion write, and `read_service` only inspects that flag when the primary type is Automatic. A dropped companion write would verify clean and leave a stale `DelayedAutostart=1` behind. The response now says what happened rather than listing what was attempted: `attempted: usize` plus `failure: Option<OpFailure>`. Success requires both no named failure and a full attempt count, so the shape that used to pass silently is now unrepresentable as a success. `OpOutcome` and the positional-results contract are gone with it. The decision moves into `check_response`, isolated from the spawn so it is testable against responses the executor would never emit, matching how `map_status` and `map_task_state` already separate a decision from its OS call.
…hat clears it `CloseHandle` overwrites the calling thread's last-error, so six failure paths were reporting the close's result rather than the call that actually failed: `LookupPrivilegeValue`, `AdjustTokenPrivileges`, `OpenProcessToken`, `DuplicateTokenEx`, `CreateProcessWithTokenW`, and `CreateProcessW`. Diagnostics only, never a false success, but "OpenProcess failed: 0" sends anyone debugging an elevation failure in the wrong direction. `wait_and_reap` already got this right; the rule is now stated in the module docs and enforced by a `close_then` helper that takes the already-built error, so the last-error cannot be read after the close. Alongside that, three simplifications in the same code: `find_process_by_name`'s hand-rolled UTF-16 compare loop and its `char_to_lower` become `from_utf16_lossy` + `eq_ignore_ascii_case`; the two `STARTUPINFOW` literals that spelled out fifteen null fields each become a shared `hidden_startup_info`; and `common.rs` no longer re-exports forty `windows_sys` symbols, so each spawn file now names the Win32 surface it actually uses. The TI path sets `cb` to `size_of::<STARTUPINFOEXW>()` explicitly after taking the shared startup info, since `EXTENDED_STARTUPINFO_PRESENT` requires the size to span the extended struct or the parent-process attribute list is ignored.
…uest file The broker wrote its op list to a predictable `%TEMP%` path with `fs::write`, closed the handle, then spawned a SYSTEM or TrustedInstaller child to read it back. Every process running as this user can write that directory, including Medium-integrity ones that never passed a UAC prompt, and the window is not tight: acquiring the TI token alone polls the service for up to ten seconds before the child even starts. Winning that race meant choosing what ran as SYSTEM. action.rs already had the fix for this exact threat, on the less privileged path. That guard moves into `services::exclusive_temp` and both paths now share it: a CSPRNG-derived filename, `create_new` so a pre-planted path fails loudly, and a write handle held open with only FILE_SHARE_READ so the intended reader can read while nothing else can rewrite, delete, or rename. Its tests pin the OS behaviour being relied on, including the ERROR_SHARING_VIOLATION a second writer now gets. The response file is a weaker case and the docs now say so rather than implying it is closed. The child creates it, so the parent cannot hold it open the same way; it gets an unpredictable path, and the exit-code gate and nonce still stand, but a same-user process that wins the post-exit race and guesses the batch length could forge a success. Handing the child an inherited pipe instead of a path is what would close it. Transport-failure exit codes are named constants now, and the parent maps each back to a phrase. Previously the child described them with `eprintln!` into a process spawned with no console and no inherited stderr, so the text went nowhere and the user saw a bare number.
…rth keeping The comments across the elevation subsystem and its call sites had drifted into recording their own development history: which task added a function, what it was called before, which file stayed untouched so an earlier review would still hold, which controller decision a test came from. Git records all of that, and a reader looking at the code today has to filter through it to reach the part that explains why the code is shaped this way. Cut the archaeology, keep and sharpen the load-bearing rationale: why an HKCU address must never become a broker op, why a batch stops at its first failure, why `wait_and_reap` keeps three wait outcomes distinct, why the `DelayedAutostart` companion write is unconditional, and why translation lives beside each address shape while dispatch lives above every kind. Three comments were also stale enough to mislead. `service.rs` and `task.rs` each had a test asserting "this build cannot yet route System/Ti through the broker", which stopped being true when that routing shipped: what those tests actually pin is that the in-process kind refuses to escalate on its own when called directly, bypassing the routing. Renamed and reworded to say that. `AccessDenied` now mentions that it also carries "the elevated child ran but the op failed", which is what a broker op failure maps to.
Section 7 still named the `trusted_installer` alias (removed) and listed `run_powershell()`, `run_powershell_as_system()`, and `run_schtasks_as_system()`, none of which exist. Replaced with the real surface: one `run_ops` entry point, typed effects only, and how the two tokens are acquired.
Issue 2 was fixed by b3ccbfd, which removed `handleSwitchChange` and made System Default its own segment routed through `goSystemDefault`, so it unstages first. The entry described a handler that no longer exists. Verified the cancel path is reachable from all three control shapes: one option, two options, and the 3+ dropdown. The diagnosis itself is still literally true and now lives beside the code it describes. `stageApply`'s cancel check compares against `activeOption`, which is null at System Default by design, so it can never fire there. Any new control that calls `stageApply` for that position would reintroduce the dead end, which is worth knowing at the call site rather than in a bug list the entry has left.
Cancelling a staged change worked by clicking whatever is already live, which `stageApply` implemented as `label === activeOption`. That comparison cannot match at System Default, where `activeOption` is null by design (ADR-0003: no option is active), so the System Default position had to be diverted to `goSystemDefault` before it ever reached staging. Both call sites did that correctly, so nothing was broken, but the rule lived in each caller rather than in the code it protects: a third control wired straight to `stageApply` would stage a change with no way back to zero pending, which is exactly the bug KNOWN_ISSUES issue 2 described. `stageApply` and `goSystemDefault` merge into one `selectTarget`, which takes the clicked target and decides. System Default is checked first, so it cannot fall through to staging no matter who calls. The two handlers now just forward, and the duplicated System Default branch in each of them is gone. No behaviour change: same three outcomes (restore, cancel, stage) for the same inputs, now reachable by one path instead of two.
Measures where the status scan actually spends its time, by wrapping the existing `EffectKind`/`ProbeSource` injection seams in a decorator that records call count and elapsed time per work category. The production path runs unmodified; nothing in detect.rs is mocked or touched. Ignored by default since it reads the live registry, SCM, and Task Scheduler COM. cargo test --release scan_sweep_timing -- --ignored --nocapture
Measured with scan_bench: 12.25s serial, 2.02s parallel, 6.1x on 16 threads. Detection is a pure read and tweaks are independent, but the sweep ran them one at a time on a single thread. That is fine for the registry (375 reads total 6ms), and ruinous for the 26 Action probes, which each spawn a PowerShell process and average 470ms. Serially those 26 are 12.22s of the 12.25s sweep. Results stream back over a channel and are emitted on the calling thread, so `emit` needs no `Send` bound and the progressive arrival the UI is built around is intact. Events now arrive in completion order rather than corpus order; each names its `tweak_id` and the frontend keys on it, so order never carried meaning. `statuses_emit_incrementally` asserted positions, which completion order makes meaningless. It now compares the sorted set of reported ids against the corpus, which is the stronger assertion anyway: indexing the first three events cannot catch a duplicate or a dropped tweak, and a set comparison can. Nothing needed locking. `ProbeCache` is already behind a mutex, the snapshot and claims stores are immutable path holders that re-read per call, and `scheduler_service` already serializes COM activation behind its own lock with a once-per-thread MTA init. rayon was already a declared dependency with no user in src/.
…rocess
Measured with scan_bench: serial 12.25s to 6.41s, parallel 2.02s to 1.19s.
Spawning probes drop from 26 to 14.
`probe:` was script-only, so every detection signal cost a process. That is 178ms
of floor before the script runs, and detection pays it once per probeable Action on
every sweep. Two whole classes did not need an interpreter at all.
Twelve probes asked "is this one package installed", each in its own PowerShell.
Measured, asking about one package costs the same as listing every package: 424ms
for `Get-AppxPackage -AllUsers -Name X` against 416ms for the bare enumeration,
because the cost is the module load plus the WinRT walk and both are per-process.
`probe: { appx_absent: [...] }` is now answered from one enumeration the whole sweep
shares, built on first ask and cached in `ProbeCache` so the same invalidation that
drops a stale probe reading drops it too.
One probe read a single DWORD, which the app reads natively in 16 microseconds.
`probe: { registry: { key, name, equals } }` covers it. Honest scope: this is the
only probe in the corpus that qualifies. The other `Get-ItemProperty` uses sit
inside subkey enumerations or alongside powercfg, and stay scripts. The form earns
its place by making the fast path available to future detection-only checks, not by
what it saves today.
`Probe::Script` stays the escape hatch and still covers the fourteen that genuinely
need an interpreter (powercfg, DISM, auditpol, CIM, Get-Process). `remove_copilot_app`
stays a script deliberately: it prefix-matches PackageName where the other eleven
match DisplayName exactly, and converting it would change detection semantics on a
machine none of us can test against.
Native forms resolve in `detect::probe_cached`, the only layer holding the shared
state they read; `ProbeSource` keeps its original job of running the script and
remains what a test mocks.
Verified against the live machine rather than assumed: all twelve package probes and
six genuinely-installed control packages agree between the old per-package queries
and the shared index, in both the present and absent directions, case-insensitively.
An enumeration that fails surfaces as Err, never as an empty set, since empty would
read as "already removed" for every asking tweak.
`probe:` now takes a map as well as a script body, so the three tables and the "inline strings only" callout that all said "the script body" were understating what the field accepts. Section 12.5 gains a "pick the cheapest form that answers the question" subsection covering `appx_absent` and `registry`, each with the measurement that motivates it, and says plainly when a script is still the right answer. The note authors most need is that asking about one package costs the same as listing every package, so hand-writing a per-tweak `Get-AppxPackage` check pays full price for an answer the shared enumeration already holds.
…es if_missing `optional` only ever guarded capture and detect, never the drive. An optional effect whose resource was absent returned ResourceMissing from the drive, which aborted the apply and rolled the whole tweak back, even though detect had already mapped that same absent resource to its `if_missing` value and reported the option as available and satisfied. Apply now agrees with detect: when an option's desired value equals the effect's `if_missing`, there is nothing to drive and nothing to verify, so the effect records as a no-op. The tolerance stays narrow; a desired value that differs from `if_missing` still fails typed and rolls back. This affected shipped tweaks, not only new ones. privacy.yaml and services.yaml already declare optional task effects that would hard-fail on any machine where one of those tasks is absent.
…view The details modal renders one section per change kind straight off TweakOptionView. If push_setting_change ever stopped projecting a kind, that section would render empty and the tweak would look registry-only while apply and detect kept working perfectly, so no existing test would catch it. Walks the whole corpus and asserts every valued Setting effect, plus every claimed shared effect, lands in exactly one of the six change lists. Also asserts the corpus genuinely exercises registry, service and scheduled-task projection, so the check cannot pass vacuously if a kind leaves the corpus.
…ock tweaks Moves the 13 Windows Update tweaks out of network.yaml into their own windows_update category; what remains is renamed "Network & Power". windows_update_mode replaces the narrower disable_auto_update_download with a four-mode AU policy switch: automatic, download only, notify only, off by policy. It owns the same two addresses, so the two cannot coexist. block_update_pipeline stops the pipeline at its roots. Disabling wuauserv alone does not hold, because the Windows Update Medic Service repairs it, so the Medic service and both of its tasks are driven first; the service registers no start triggers, so once those tasks are disabled it has no way to run. The tweak also covers the usoclient scan tasks, the MusNotification prompt, the update UI, and the network path, while deliberately leaving BITS, Delivery Optimization and the Store install tasks alone. Every scheduled-task effect runs at TrustedInstaller. On stock 24H2 the UpdateOrchestrator task files grant Administrators read and execute only, and the WaaSMedic pair carries a protected DACL, so an elevated administrator alone cannot disable them; a too-low level would be a hard failure plus full rollback.
…emantics
- "eight category files" is now nine, in ARCHITECTURE, TWEAK_SYSTEM and
TWEAK_AUTHORING
- CLAUDE.md pointed at models/tweak_schema.rs, which does not exist on this
branch; the live schema is src/tweaks/{model,parse,schema,validate}.rs
- CLAUDE.md claimed the elevation broker runs PowerShell via -EncodedCommand.
BrokerOp carries no script variant; PowerShell runs only through the action
effect kind
- CLAUDE.md still described skip_validation and the "2 options to toggle" rule,
neither of which exists in the current schema
- TWEAK_AUTHORING section 8.4 said only that a vanished resource fails typed,
which did not cover the desired-equals-if_missing case now applied as a no-op
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.