Open
Conversation
Runs pytest on every push to master and on every pull request. Uses windows-latest because the toolbox is Windows-only (ctypes.windll, WMI, Tk bundled). Installs requirements.txt + the runtime deps the tests need at import time; the full pin is deferred to Plan B-B4.
- Add self._filter_lock (threading.RLock) guarding _active_categories and _active_severities. - Add _snapshot_filters() helper returning immutable frozenset pair under the lock. - Wrap mutations in _toggle_category / _toggle_severity with the lock. - Route every read site through _snapshot_filters(): _load_historical, _filter_historical, _export_historical, _filter_live, _start_live_poll, and the _process_queue "live" hot path (previously L2275-2283). - Tests: 4 new cases in tests/test_account_activity_monitor.py cover frozenset type, post-snapshot mutation isolation, concurrent reader/writer with no RuntimeError, and snapshot immutability. Closes A4 from plans/2026-04-17-debt-review-PLAN_A_critical.md.
…ity_monitor.py Closes B1l from plans/PLAN_B_sweep.md. Enables A7 consolidation of the 4th portable launcher (previously blocked by the space-containing module name, which could not be imported via `from tools.X import App`). Main.py uses AST discovery (no filename hardcoded) and the UI display name comes from TOOL_NAME in the module body, so the rename is transparent to users. Updated 6 references: - tools/_runner.py docstring + comment (example now references lowercase filename; spaces-in-path support is still tested via tmp-fixture in test_runner_handles_spaces_in_filename). - tests/test_common_threadsafe.py comment. - tests/test_tool_runner.py docstring. - HANDOFF.md "Where to Find Things" table. - SPEC.md Tools table. Audits/ and plans/ retain the historical filename by design.
Delete 4 duplicate tools/*.py under portable/ (3 silently diverged; network_stability_monitor.py was missing A5 BoundedDeque). Launchers now import from the canonical tools.* package. PyInstaller specs set pathex=repo-root and hiddenimports=['tools.<module>', 'tools._common.threadsafe']. Gitignore exception for portable/build/*.spec so the specs are versioned (build artefact subdirs stay ignored). Drift guard test prevents reintroduction. HANDOFF/SPEC docs updated. EXE rebuild + launcher smoke-tests remain with the user.
_delete_dir_contents now runs os.walk(followlinks=False) against any directory entry it is about to rmtree/send2trash; if ANY symlink is found below, the entry is refused entirely and a structured symlink_in_tree_refused log is emitted (path + offender). Stricter than rmtree stop-at-link behavior -- a louder, auditable signal that matches the Plan A/A1 stance. Also: every successful delete now emits a trashed or permanent_deleted structured line with path + byte count, closing the log-each-deletion -outcome bullet from the plan. New helper: _tree_has_symlink(root) -> Optional[str] returns the first offender path so the caller can log it. Tests: nested-symlink refusal, helper happy/sad paths, structured success log. Symlink-creation cases skip when the dev env lacks Windows symlink privilege (matches the pre-existing skip pattern). PERMANENT second-confirmation (plan bullet) is a no-op here: the UI hardcodes DEFAULT_DELETE_MODE = TRASH so PERMANENT is unreachable from buttons; the CLI --permanent path already requires both --yes and --categories (stricter than one askyesno). No gap to close.
Adds the four missing helper modules the Tier B sweep depends on so
B1-B3 can proceed surgically. Each module centralises a pattern
currently duplicated across ~10 tools:
* paths.py -- REPO_ROOT / TOOLS_DIR / TESTS_DIR / PLANS_DIR /
AUDITS_DIR / COMMON_DIR / PORTABLE_DIR. One
derivation, imported everywhere.
* subprocess.py -- CREATE_NO_WINDOW + run_hidden / popen_hidden
wrappers that OR-merge caller flags so the
hidden-window bit never gets dropped.
(Replaces the _CNW / _CREATE_NO_WINDOW / inline
0x08000000 zoo.)
* ui_theme.py -- apply_dark_treeview_style(master) centralises the
dark Treeview palette duplicated in 5+ tools.
* exceptions.py -- narrow_excepts(*types) decorator and
suppress_and_log context manager. Pick per-site:
decorator for whole-function degradation, context
manager for a few lines.
_common/__init__.py now re-exports the commonly-used names so callers
can do `from tools._common import TOOLS_DIR, run_hidden`. Submodule
imports remain valid and preferred for narrow imports.
Tests: test_common_paths.py + test_common_subprocess.py (11 new
tests). ui_theme and exceptions are thin enough that their smoke
coverage lives in downstream usage sites in B1-B3 rather than
stand-alone tests here.
Zero callsite changes in this commit -- foundation only. B1 will
start migrating tools onto the new helpers.
29 tests covering pure helpers and JSON persistence: decision_dice (15): hex_to_rgb/rgb_to_hex clamp+float-floor, lerp_color endpoints, build_pool weighted expansion + zero/negative-weight fallbacks, load/save_profiles + load/save_journal (missing-file, roundtrip, corrupt-JSON, 500-entry cap), play_sound unknown-kind + daemon-thread smoke. folder_size_analyzer (14): format_size 0 B / B / KB / MB / GB / TB boundaries + mid-unit rounding, format_date zero-sentinel + known timestamp, get_folder_size totals / hidden-dir skip / empty-dir ctime-sentinel / progress callback cadence, get_drive_info valid + bogus path, FolderInfo defaults + documented always-True __lt__ quirk. No UI instantiation — imports ctk at module load, exercises pure helpers and fs roundtrips against tmp_path. Full suite: 163 pass (pre-existing screen_lock failures are Chris's WIP, not introduced here).
…moke 88 new tests across three previously-untested tools: security_audit (17): now_ts shape, _age_days recent + missing-sentinel, _is_suspicious_path safe-override + suspicious buckets, Finding.key stability / dedup window / severity-ignorance, SecurityAuditEngine state load (missing + corrupt) + save/reload roundtrip, baseline lifecycle (save/get/clear), get_checks returns all 10 category callables bound to the engine. network_pattern_analyzer (18): _parse_timestamp strict-format + None on bad input, _calculate_duration_seconds + _get_date_range, full coverage of _analyze_* methods (summary, time-distribution 24 buckets, category frequency with UNKNOWN fallback, average duration per-category, repeating time windows above-average filter, sequential correlations < 10min pair detection). Uses __new__ to skip the Tk root. ffmpeg_studio (53): _crf_label bucket boundaries + out-of-range empty, _guess_ratio h=0 guard + 11 known ratios + custom fallback, GPU/codec label parsers, _chroma_depth_keys, _pix_fmt dispatch incl. NVENC yuv444p16le quirk + AMD 444+10-bit p010le fallback, _check_compat_warning for AMD AMF 4:4:4, _build_video_codec_args covers CPU/NVENC/AMF/QSV branches + AMD QP clamp + unknown-speed fallback + unknown-GPU fallback. Subprocess-dependent helpers (_test_encoder, _probe_hardware, _detect_audio_devices, SecurityAuditEngine.check_*) deliberately skipped — need ffmpeg/registry/psutil mocking, out of scope for B4 smoke. Full suite: 251 pass (pre-existing screen_lock failures unchanged).
Adds get_config / get_bool / get_path helpers with optional python-dotenv fallback at <repo>/.env. Real env vars beat .env; defaults beat both. .env.example documents the AUTOMATIONS_* keys the per-tool swaps will use. 26 unit tests cover precedence, bool parsing, Path expansion, and the one-shot dotenv loader. Re-exported from tools._common. Sets up B3 sweep of hardcoded constants across system_cleaner, network_stability_monitor, and ffmpeg_studio.
Replaces 3 hardcoded C:/Windows/... literals in the cleaner category table with os.path.expandvars() — matches the existing %LOCALAPPDATA% pattern already at L466 and handles non-C: Windows installs. - L448 win_temp -> %SystemRoot%/Temp - L475 prefetch -> %SystemRoot%/Prefetch - L485 win_update -> %SystemRoot%/SoftwareDistribution/Download No behavior change on standard Windows installs. No new imports.
Moves three module-level defaults to tools._common.config lookups so they can be overridden via env or .env without editing source: - AUTO_EXPORT_ENABLED -> AUTOMATIONS_NSM_AUTO_EXPORT (default True) - AUTO_EXPORT_TIME -> AUTOMATIONS_NSM_EXPORT_TIME (default 23:30) - EXPORT_FOLDER -> AUTOMATIONS_NSM_EXPORT_DIR (default exports) Defaults match the prior hardcoded values exactly — no behavior change for zero-config users. Documented in .env.example.
Moves the three ctk.StringVar defaults for the Record/Capture/Convert tabs to tools._common.config.get_path lookups so each can be overridden independently via env or .env: - output_dir_var -> AUTOMATIONS_FFMPEG_OUTPUT_DIR (default ~/Videos) - cap_out_var -> AUTOMATIONS_FFMPEG_CAPTURE_DIR (default ~/Pictures) - conv_out_var -> AUTOMATIONS_FFMPEG_CONVERT_DIR (default ~/Videos) Existing os.path.expanduser call is dropped — get_path handles tilde expansion on both env values and Path defaults. Defaults match prior behavior for zero-config users.
- winotify toast dedup + persisted notif state (T10-T12) - rotating session scan + 1h live-window liveness (T14) - flat session list, Last Turn tokens column - Cost Composition + Turn Stats panels in Session Detail - 72/72 tests green
Pilot for Plan B B2 broad-except sweep on the smallest tested tool (433 LOC, 6 sites). Validates @narrow_excepts + inline-tuple split. Triage: load_exports inner -> (OSError, json.JSONDecodeError, UnicodeDecodeError) load_exports outer -> (OSError, tk.TclError) analyze_patterns -> (KeyError, ValueError, TypeError, AttributeError) export_analysis -> (OSError, TypeError) _parse_timestamp -> @narrow_excepts(ValueError, TypeError, default=None) run_tool boundary -> broad kept + log.exception + noqa BLE001 Findings for the remaining 9 files in the sweep: - @narrow_excepts decorator fits single-expression helpers cleanly (1/6 sites here); most sites need inline tuple narrow because their bodies continue with cleanup / UI updates after the except. - Entry-point "boundary swallow" pattern (run_tool) stays broad but is now logged via log.exception with an inline noqa. This is the precedent for the other 9 tools. - Pre-existing print(...) error logging in load_exports left untouched (surgical rule - drift not in scope for B2). Tests: tests/test_network_pattern_analyzer.py 18/18 pass. Full suite 310 pass / 4 skip / 1 unrelated flaky security_audit _age_days (Windows mtime vs time.time() precision race, not caused by this change).
4 broad-except sites triaged (bare `except:` at get_drive_info replaced - was catching KeyboardInterrupt and SystemExit too): get_drive_info -> @narrow_excepts(OSError, PermissionError, default=...) _scan_directory -> (OSError, PermissionError, TypeError) _export_results -> (OSError, TypeError, ValueError) run_tool boundary -> broad kept + log.exception + noqa BLE001 Decorator fit: 1/4 sites (get_drive_info is a textbook single-return helper - psutil.disk_usage wrapped with a zeroed default). Tests: tests/test_folder_size_analyzer.py 14/14 pass (including test_get_drive_info_returns_zeros_for_bogus_path which exercises the decorator's default-return path).
6 broad-except sites triaged across sound + persistence + boundary: _beep_thread -> (OSError, RuntimeError, ValueError) load_profiles -> (OSError, json.JSONDecodeError, UnicodeDecodeError) save_profiles -> @narrow_excepts(OSError, TypeError) load_journal -> (OSError, json.JSONDecodeError, UnicodeDecodeError) save_journal -> @narrow_excepts(OSError, TypeError) run_tool boundary -> broad kept + log.exception + noqa BLE001 Decorator fit: 2/6 (save_profiles + save_journal are single-try-pass writers). load_profiles/load_journal kept inline because the except falls through to a different return path (defaults dict/list). Tests: tests/test_decision_dice.py 15/15 pass, including the corrupt- JSON paths that validate the narrow exception tuples catch the right types. Full suite 311 pass / 4 skip.
20 broad-except sites narrowed across 4 categories:
Subprocess probes/runs (6 sites):
_test_encoder -> @narrow_excepts(OSError, subprocess.*, TimeoutExpired, False)
_probe_hardware -> (OSError, subprocess.SubprocessError)
_detect_audio_devices -> @narrow_excepts(OSError, subprocess.*, default=[])
start-record error -> (OSError, subprocess.SubprocessError, ValueError)
capture subprocess -> (OSError, subprocess.SubprocessError, ValueError)
convert subprocess -> (OSError, subprocess.SubprocessError, ValueError)
File I/O (8 sites): all -> OSError (log read/close, os.remove cleanup)
Process termination (1): -> (OSError, BrokenPipeError, TimeoutExpired)
Tk UI (5 sites): w.configure, ov.destroy, PhotoImage -> tk.TclError
(PhotoImage also catches OSError for missing bg file)
Decorator fit: 2/20 (_test_encoder, _detect_audio_devices - both are
capability-probe helpers with a clear single default return).
No boundary pattern needed: run_tool() at line 339 is not wrapped in
try/except (CTkToplevel has its own error handling).
Tests: tests/test_ffmpeg_studio.py 53/53 pass.
21 broad-except sites narrowed across the 10-category scanner:
Top-level helpers (3):
safe_run fallback -> (OSError, subprocess.SubprocessError)
is_admin -> @narrow_excepts(AttributeError, OSError, False)
_age_days -> @narrow_excepts(OSError, ValueError, 9999)
State persistence (2):
_load_state -> (OSError, json.JSONDecodeError, UnicodeDecodeError)
save_state -> (OSError, TypeError)
Category checks (13):
scheduled tasks outer -> (OSError, subprocess.SubprocessError, csv.Error)
scheduled tasks inner -> (KeyError, ValueError, AttributeError)
process signature -> (OSError, TypeError, AttributeError)
process name lookup -> (psutil.NoSuchProcess/AccessDenied/Zombie, OSError)
RDP winreg -> (OSError, FileNotFoundError)
firewall netsh -> (OSError, subprocess.SubprocessError, ValueError)
file walk splitext -> (TypeError, AttributeError)
file walk encode -> (AttributeError, UnicodeError)
proxy winreg -> (OSError, FileNotFoundError)
cert date parse -> (ValueError, TypeError)
cert store outer -> (OSError, subprocess.SubprocessError, ValueError)
PAC winreg -> OSError
event log loop -> (OSError, subprocess.SubprocessError, ValueError)
Export + boundaries (3):
export JSON -> (OSError, TypeError, ValueError)
future.result (scan) -> broad + log.exception + noqa BLE001
_safe_check wrapper -> broad + log.exception + noqa BLE001
The two ThreadPool boundaries stay broad on purpose: a category check
can raise anything (subprocess, winreg, psutil, socket...) and must not
take down the scanner; each is logged via log.exception for forensics
and converted into an INFO Finding.
Decorator fit: 2/21 (is_admin + _age_days - both single-return helpers
with clear sentinel defaults).
Tests: tests/test_security_audit.py 17/17 pass, including the
_age_days + _load_state corrupt-JSON paths that validate the narrow
tuples catch the right types.
24 broad-except sites -> 1 boundary + 3 @narrow_excepts + 20 inline tuples. Inline pairs reflect real call-site surfaces: - ctypes.windll blocks -> (AttributeError, OSError) - subprocess.run/Popen -> (OSError, subprocess.SubprocessError) - psutil iter + proc.info -> (psutil.Error, AttributeError) - Tk widget teardown races -> (tk.TclError, AttributeError) - send2trash delete -> OSError (TrashPermissionError subclasses it) - _log_struct JSON dump -> (OSError, TypeError, ValueError) Decorator-fit helpers (single-return, no state): - is_admin, _enable_privilege -> default=False on (AttributeError, OSError) - _get_system_uptime -> default=timedelta(0) on (psutil.Error, OSError) Boundary: - _restart_explorer_worker wraps taskkill + psutil iter + explorer.exe spawn; keeps broad except with log.exception + noqa: BLE001 + UX message. Also narrowed the send2trash import guard to ImportError. Decorator fit: 3/24 (12.5%). Tests: 311 passed, 4 skipped - full suite unchanged.
…nshot outputs Fixes L1/L2 from audits/2026-04-17-security-probe.md (both Critical). User-tainted tkinter Entry flowed into subprocess.Popen/run for FFmpeg with no validation, letting any UI-reachable attacker drop files outside the intended base (Startup folders, PATH dirs, %TEMP%). - New module-level _validate_output_dir + OutputDirError: rejects empty input, '..' segments, and relative paths. Optional allowed_bases kwarg for strict containment (used by tests/PoCs). - Wrap start_recording and _do_capture call sites with try/except OutputDirError -> messagebox + early return. - 13 regression tests (unit + subprocess-invoked PoCs) all green. - Patch L1/L2 PoCs: L1 inserts repo root on sys.path, L2 switches to allowed_bases= matching the consolidated helper. Deviation from plan: default is NOT Path.home()-only containment, which would break F:\ / D:\ output workflows. Callers pass allowed_bases= explicitly when strict home containment is needed.
…-zero check Bound 0 <= x < 0.01 occasionally fails on Windows when the file mtime read outpaces the timer tick, returning a tiny negative (~1e-12). abs(x) < 0.01 preserves the "near zero" intent without forbidding the jitter.
…ck_key
block_key() only accepts single key names; combo strings like "alt+tab",
"alt+f4", "ctrl+shift+esc", "win" were silently raising
ValueError('Unsupported key name: ...') on every install. The
suppress=True hook already intercepts these combos at the OS level, so
removing them from _CRITICAL_COMBOS / _EXTRA_COMBOS is safety-neutral.
Critical combos now: ("left windows", "right windows") -- both valid
single key names that succeed.
Tests updated to exercise the new contract:
- test_install_hook_fails_fast_if_critical_combo_unsupported now
triggers fail-fast on "left windows" instead of "alt+tab".
- test_install_hook_rollback_unblocks_on_critical_failure now
exercises rollback after 1 success + 1 failure (only 2 critical
combos total) instead of 2+1.
Also: gitignore security/poc/ to prevent stray PoCs from being tracked.
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.
Summary
.github/workflows/test.yml— runspyteston every push tomasterand every PR.windows-latest(toolbox is Windows-only:ctypes.windll, WMI, bundled Tk).requirements.txt+ the runtime deps the test suite imports at module load (customtkinter,psutil,pillow,scapy,requests,pywin32-ctypes).Test plan