Skip to content

feat(safety): check launches against the resolved executable, including .lnk targets - #529

Open
FaberVi wants to merge 10 commits into
CursorTouch:mainfrom
FaberVi:pr/safety-2-launch-allowlist
Open

FaberVi wants to merge 10 commits into
CursorTouch:mainfrom
FaberVi:pr/safety-2-launch-allowlist

Conversation

@FaberVi

@FaberVi FaberVi commented Oct 31, 2026

Copy link
Copy Markdown

Summary

The App launch check now resolves what will really run before matching it against the allowlist: PATH-only lookup for bare names, exact executable matches preferred over Start Menu names, and .lnk shortcuts resolved to their target (and launched through that target). Refusals are reported uniformly as safety_denied with consistent audit rows. Absolute .exe/.lnk paths are resolved directly; bare .lnk/.exe/.com names are rejected. Path errors return is_error=true and are audited with result=error. Denials and path errors are logged as a single warning line without a traceback. subprocess.Popen is looked up at call time so it can be patched in tests.

Motivation

Matching only the name the agent typed could be bypassed (a shortcut or a same-named file in the working directory pointing at something else). Checking the resolved executable closes that gap.

Dependencies

Depends on #528 (stacked: this branch contains its commits). Please merge #528 first; the diff here will shrink to only this PR's commits once it is merged.

Testing

Testing: Linux run with Windows-only modules stubbed (pytest, Python 3.14). tests/test_safety_launch.py and the updated gate/policy tests pass (+29 tests vs A1). No new failures or collection errors compared with upstream main 7fd39b4 (on main the Linux harness has 6 known failures and 75 modules that cannot be collected without Windows). Also tested on a real Windows 11 desktop (LegionVins) with the Windows-only test modules and manual end-to-end runs.

…ions, audit log, kill switch

Add a SafetyGate FastMCP middleware (src/windows_mcp/safety/) that runs before the
desktop-control gate on every tool call. All protections are enabled by default and
configured under [safety.*] in config.toml:

- App/process allowlist for input tools (window under each point or foreground
  window), App launch/switch/resize and Process kill; unknown targets fail closed.
- PowerShell disabled by default; "allowlist" mode accepts a single listed cmdlet and
  rejects chaining, substitution, redirection, quoting and encoded/nested commands.
- Native MessageBoxTimeoutW confirmation (default No, timeout = deny) before process
  kill, closing/deleting shortcuts, FileSystem delete/move/overwrite, Registry
  set/delete, installer launches, UAC targets and risky cmdlets.
- JSONL audit log of every call and decision, with redaction and optional screenshot.
- Kill switch via flag file or latching global hotkey.
- HTTP/SSE transports require a bearer token (server.require_auth) and refuse
  non-loopback binds unless server.allow_remote_bind is set.

write_config now round-trips the [safety] section. Windows calls are isolated in
safety/win32.py; unit tests use a fake platform and run on Linux. Docs: docs/safety.md.
…uniform audit rows

- App launch: resolve the fuzzy Start Menu match to its real executable (exe path,
  .lnk target or packaged app manifest) and check that against the allowlist; deny
  when unresolvable. After launch, re-check the returned pid and terminate only that
  process if it is not allowed. Fixes "notepad" starting notepad++.exe on Italian
  Windows (Notepad is listed as "Blocco note").
- launch_executable: resolve bare names through absolute PATH entries only, never the
  current directory, and apply the allowlist to the resolved path.
- Audit: every row has the same keys (reason/result/error null when not applicable);
  duration_ms is measured for refusals too and includes confirmation wait.
- Docs and tests updated.
…ches, check .lnk targets

- Start Menu launch refusals (pre-launch target check and post-launch kill) now raise
  LaunchDenied; SafetyGate returns a safety_denied tool error and audits decision=denied
  instead of a successful result.
- Start Menu matching prefers an exact display name, then an entry whose executable
  stem equals the request (so "notepad" picks localized "Blocco note"), then fuzzy.
- launch_executable on a .lnk is checked on the shortcut's target executable.
- Update test_app_tool for the new missing-executable message; docs and tests.
launch_executable on a .lnk no longer passes the shortcut to CreateProcess. It reads
the shortcut's target, arguments and working directory and starts the target with
them (caller args appended, explicit cwd wins). The target must be an existing .exe
or .com; it is re-checked against the allowlist right before start and the started
pid is verified afterwards (only that pid is terminated if not allowed). Docs and tests.
The default popen argument bound subprocess.Popen at definition time, so tests that
monkeypatch app.subprocess.Popen had no effect (WinError 193 on Windows).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant