Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d87f388
Give the GUI a searchable navigation panel, token-based dark and ligh…
JE-Chen Oct 8, 2026
8ab69ab
Update mypy requirement from >=2.3.0 to >=2.4.0
dependabot[bot] Oct 8, 2026
cf14846
Merge remote-tracking branch 'origin/main' into feat/gui-modernization
JE-Chen Oct 8, 2026
2a8d2ce
Load the pytest plugin from a top-level module so pytest start-up no …
JE-Chen Oct 8, 2026
6817b38
Stop rewriting sent agent turns; restart the history from a summary i…
JE-Chen Oct 8, 2026
e62e9d9
Keep the action signing key apart from the right to execute
JE-Chen Oct 8, 2026
6f32e82
Raise the cryptography floor to 50.0.0
JE-Chen Oct 8, 2026
440f3fb
Refuse unlisted packages by default at the package gate
JE-Chen Oct 8, 2026
cb7e57b
Give every top-level run its own variable scope
JE-Chen Oct 8, 2026
fe599ea
Carry the click count on macOS mouse events so clicks=2 is a double-c…
JE-Chen Oct 8, 2026
0909604
Look a macOS window up by id so a minimized one can be listed and res…
JE-Chen Oct 8, 2026
f99b2a2
Record who opened each remote-desktop host and viewer so panels stop …
JE-Chen Oct 8, 2026
d15d610
Pair USB passthrough replies with their request by an echoed request id
JE-Chen Oct 8, 2026
eeb60e2
Report window focus, show and z-order failures and stop layouts drifting
JE-Chen Oct 8, 2026
a6677b0
Capture every monitor in the coordinates the mouse takes
JE-Chen Oct 8, 2026
64f0e38
Read templates at any path and in grayscale, keep the left OCR box, f…
JE-Chen Oct 8, 2026
5690d29
Type capitals and Shift as written, one Enter per line break, and scr…
JE-Chen Oct 8, 2026
4511bfd
Type the cross-platform names mypy 2.4 could not, and check with 2.4 …
JE-Chen Oct 8, 2026
2191d1f
Merge remote-tracking branch 'origin/dependabot/pip/mypy-gte-2.4.0' i…
JE-Chen Oct 8, 2026
d85f868
Merge branch 'sweep/signing-crypto' into feat/progress-sweep
JE-Chen Oct 8, 2026
6946f79
Merge branch 'sweep/agent-packages' into feat/progress-sweep
JE-Chen Oct 8, 2026
7d2cd87
Merge branch 'sweep/variable-scope' into feat/progress-sweep
JE-Chen Oct 8, 2026
b108c92
Merge branch 'sweep/macos' into feat/progress-sweep
JE-Chen Oct 8, 2026
2cbfb82
Merge branch 'sweep/remote-registry' into feat/progress-sweep
JE-Chen Oct 8, 2026
95c01ed
Merge branch 'sweep/usb-request-id' into feat/progress-sweep
JE-Chen Oct 8, 2026
d898c45
Let an MCP server be confined to root directories and keep host-pushe…
JE-Chen Oct 8, 2026
27a3b67
Merge branch 'sweep/input-image' into feat/progress-sweep
JE-Chen Oct 8, 2026
82fb01f
Mark the tab registry's import as table-driven for the static analyser
JE-Chen Oct 8, 2026
fae3346
Authorise REST routes and MCP tools by role when a user store is conf…
JE-Chen Oct 8, 2026
70cb448
Keep the analyser marker within the line limit
JE-Chen Oct 8, 2026
25cd331
Merge branch 'feat/gui-modernization' into feat/progress-sweep
JE-Chen Oct 8, 2026
0cafb6a
Merge branch 'sweep/windows-capture' into feat/progress-sweep
JE-Chen Oct 8, 2026
0b472eb
Merge branch 'sweep/path-policy' into feat/progress-sweep
JE-Chen Oct 8, 2026
4a41551
Re-measure the module map after merging the sweep branches
JE-Chen Oct 8, 2026
acfe322
Merge branch 'sweep/rbac' into feat/progress-sweep
JE-Chen Oct 8, 2026
9414cb8
Record the Progress sweep and confirm the foreground only where the b…
JE-Chen Oct 8, 2026
cd7be7a
Find a minimised macOS window through the full list when the by-id qu…
JE-Chen Oct 8, 2026
6abb2a3
Keep the analyser marker within the line limit
JE-Chen Oct 8, 2026
95e3d9c
Record what the macOS runners verified and what still needs a Mac
JE-Chen Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/platform-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ jobs:
# back to building from source and CMake could not
# configure for ARM64. Twelve minutes, then failure.
# cryptography — wheels stop at 46.0.3; 46.0.4 onwards ship none.
# Our floor is >=48.0.1 and that is a security floor
# (GHSA-537c-gmf6-5ccf), so it cannot be lowered.
# Our floor is >=50.0.0 and that is a security floor
# (GHSA-537c-gmf6-5ccf, GHSA-g6cj-pr64-35w5), so it
# cannot be lowered.
#
# Nothing in the package imports either one at import time, so what
# this square proves is real: the install succeeds and the stable API
Expand All @@ -38,7 +39,7 @@ jobs:
#
# pip install --dry-run --only-binary=:all: --platform win_arm64 \
# --python-version 3.12 --target /tmp/probe \
# 'opencv-python>=4.8,<6' 'cryptography>=48.0.1'
# 'opencv-python>=4.8,<6' 'cryptography>=50.0.0'
#
# When both resolve, drop the markers from pyproject.toml.
os: [windows-2022, ubuntu-22.04, macos-14, ubuntu-22.04-arm, windows-11-arm]
Expand Down
17 changes: 10 additions & 7 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
run: "pip install --only-binary :all: ruff==0.16.0"

- name: Run ruff
run: ruff check je_auto_control/
run: ruff check je_auto_control/ je_auto_control_pytest.py

security:
runs-on: ubuntu-latest
Expand All @@ -57,7 +57,7 @@ jobs:
run: "pip install --only-binary :all: bandit==1.9.4"

- name: Run bandit (recursive, skip tests + i18n dicts)
run: bandit -r je_auto_control/ -c pyproject.toml
run: bandit -r je_auto_control/ je_auto_control_pytest.py -c pyproject.toml

pytest-headless:
# The suite ran on Windows alone for its whole life, so every
Expand Down Expand Up @@ -150,10 +150,13 @@ jobs:
# would hide exactly the breakage this job exists to find.
#
# `coverage run -m pytest`, NOT `pytest --cov`, and that is load-bearing:
# this package registers a `pytest11` entry point, so pytest imports
# `je_auto_control.utils.pytest_plugin.plugin` while loading plugins —
# which executes `je_auto_control/__init__.py`, the facade, and with it
# several hundred modules. pytest-cov only starts measuring after that,
# this package registers a `pytest11` entry point, and while it pointed
# at `je_auto_control.utils.pytest_plugin.plugin` pytest imported that
# submodule while loading plugins — which executes
# `je_auto_control/__init__.py`, the facade, and with it several hundred
# modules. (The entry point is the top-level `je_auto_control_pytest`
# now; `coverage run` stays because it does not depend on what a plugin
# imports.) pytest-cov only starts measuring after that,
# so every one of those modules' import-time lines was recorded as never
# executed. Measured on this tree: 11,962 statements, 24 points, and the
# worst-hit files were the biggest ones (`action_executor` +786,
Expand Down Expand Up @@ -196,7 +199,7 @@ jobs:
with:
python-version: "3.12"
- run: pip install -e . # NOSONAR githubactions:S8541,githubactions:S8544 # reason: installs the checked-out project itself, there is no upstream version to lock and the build must run
- run: "pip install --only-binary :all: mypy==2.3.0"
- run: "pip install --only-binary :all: mypy==2.4.0"
# Deliberately NOT installing the optional extras: the contract forces
# every non-base third-party module to `Any` so the result cannot depend
# on what is installed, and installing them here would only hide a
Expand Down
131 changes: 129 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,44 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's

### Added

- Action files can be signed with an Ed25519 key pair, so an endpoint that
verifies does not hold what signs: `create_signing_keypair(private_path,
public_path)` / `AC_create_signing_keypair`, `private_key_path=` on
`sign_action_file`, `public_key_path=` on `verify_action_file`, and the
environment variables `JE_AUTOCONTROL_ACTION_SIGNING_PRIVATE_KEY`,
`JE_AUTOCONTROL_ACTION_SIGNING_PUBLIC_KEY` and
`JE_AUTOCONTROL_ACCEPT_LEGACY_ACTION_SIGNATURES`. Nothing changes while none
of them is set. `CryptographyUnavailableError` (an `AutoControlException` and
a `RuntimeError`) replaces the bare `RuntimeError` raised without the
`cryptography` package.
- Opt-in roles for the REST API and the MCP HTTP transport:
`JE_AUTOCONTROL_RBAC_USERS=<user store file>` (or `user_store=`, or `--users`
on the REST entry points). A bearer token then names a user, each route,
tool and privileged `AC_*` command needs a capability of that user's role,
and audit entries carry the user id. `UserStore`, `UserRecord`,
`AuthorizationContext`, `AuthorizationError`, `UserAuthError` and
`authorization_scope` are exported; `Capability.SIGN_ACTIONS` is new.
- Opt-in confinement of MCP file arguments: `JE_AUTOCONTROL_MCP_PATH_ROOTS`,
`JE_AUTOCONTROL_MCP_PATH_ROOTS_FROM_CLIENT` and, for `env://` references,
`JE_AUTOCONTROL_MCP_ENV_REF_ALLOW`. Tool schemas mark file-path arguments
with `"format": "path"`. `PathPolicy` and `PathNotAllowedError` are exported.
- `execution_scope(variables=None)`: a fresh variable scope for a block of
runs.
- `JE_AUTOCONTROL_ALLOWED_PACKAGES` and `je_auto_control run --allow-package
NAME` allow packages at the package gate without Python glue.
- USB passthrough requests carry an optional `request_id` that a current host
echoes; `UsbClientDesynchronized`, `ClientHandle.reusable`,
`UsbPassthroughClient.reusable`.
- `remote_desktop_registry` records who opened each host and viewer
(`adopt`, `evict`, `release`, `owner_of`, `owned`; `owner=` on the stop and
disconnect methods), and every `*_status` result gains an `"owner"` key.
- `mark_screen` returns `image_origin`; `render_marks(..., origin=)`.
- GUI: a navigation panel on the left of the main window lists every tab by
category, with a search box (`Ctrl+K`; `Ctrl+B` hides the panel).
`AutoControlGUIWidget.activate_tab(key)` opens a tab or brings it to the
front, and `current_tab_key()` names the tab on screen.
- GUI: **View → Theme** switches between a dark and a light theme, both built
from the tokens in `gui/theme.py` (`AutoControlGUIUI.set_theme(name)`).
- AI-agent documentation and a dedicated `AI.md` explain computer-use positioning, MCP aliases, safe tool selection, and OpenAI integration.
- `AC_run_agent` now uses a focused computer-use allow-list by default instead of exposing the full `AC_*` command catalogue to the model.
- `write_secret(secret)` / `AC_write_secret` (`secret`): type a password or
Expand Down Expand Up @@ -87,8 +125,8 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's
in one call: the clicks land on one point, `interval` seconds apart.
Defaults keep the single click, and a single click records the same
action as before. `AC_click_mouse`, the `ac_click_mouse` MCP tool and the
Script Builder take both parameters. On macOS the clicks still arrive as
separate single clicks (`Progress.md`).
Script Builder take both parameters. On macOS the n-th click sets the
click-state field (unverified on hardware, `Progress.md`).
- **Drag pacing: `tween_drag` / `drag_path` take `step_delay_s` and
`settle_s`** (seconds, default 0). `step_delay_s` rests after each move;
`settle_s` rests on the start before the press, after the press and before
Expand Down Expand Up @@ -155,6 +193,95 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's

### Changed

- **Breaking: the package gate refuses by default.**
`AC_add_package_to_executor` / `AC_add_package_to_callback_executor` (and
the `package_manager` methods) no longer import a package that has not been
allowed; the `DeprecationWarning` release is over. Allow with
`executor.allow_packages(...)`, `JE_AUTOCONTROL_ALLOWED_PACKAGES` or
`--allow-package`; `executor.set_allow_arbitrary_packages(True)` restores
the old behaviour. `allow_packages` now rejects a name that is not a dotted
module name.
- **Breaking: a file a remote-desktop host pushes lands below the viewer's
download directory.** `host.send_file_to_viewers(src, dest_path)`:
`dest_path` is now relative to `~/Downloads/AutoControl`
(`JE_AUTOCONTROL_REMOTE_DOWNLOAD_DIR`); an absolute path, a drive or UNC
path, `..` or a link leading outside fails the transfer. Viewer → host is
unchanged, and `FileReceiver()` without `base_dir` is unconfined as before.
- **Variables no longer survive a run.** `execute_action_with_vars`, REST
`/execute` and `/execute_file`, every MCP tool call, each socket command,
scheduler jobs, trigger / hotkey / webhook / e-mail firings, ChatOps `/run`
and voice commands each get a fresh variable scope; a later `${name}` fails
with `Unknown variable` instead of reading the previous caller's value.
Direct `executor.execute_action(...)` from Python keeps the process scope.
An `AC_parallel` branch starts with a copy of the caller's variables, and
`AC_run_dag` local nodes run on the calling list's executor and scope.
- **Windows is per-monitor DPI aware.** Importing the package asks for
per-monitor v2 (falling back to system awareness). On a monitor whose scale
differs from the primary's, coordinates and capture sizes are now physical
pixels: a point at an offset from that monitor's corner moves by
`its scale / primary scale`, and its screenshots are no longer downscaled.
Coordinates, regions, window layouts and templates recorded on such a
monitor have to be recorded again; the primary monitor is unaffected.
- **Keyboard input on Windows and X11.** `write("Hi")` types `Hi` (it typed
`hi`); `is_shift=True` is honoured by `type_keyboard`, `hotkey`, `write` and
`press_keyboard_key`; CR LF is one Enter; `write_secret` and the Unicode
typing helpers press Return / Tab / Backspace as keys instead of sending
them as code points.
- **`mouse_scroll` defaults to `scroll_direction="scroll_up"`**, so a positive
amount scrolls up on every platform; on X11 / Wayland it used to scroll
down. NaN, infinite and non-numeric coordinates raise before anything moves,
and fractional coordinates are rounded rather than truncated.
- **Window management on Windows.** `focus_window` raises
`AutoControlActionException` when the window did not become the foreground
one; `show_window_by_title` and the z-order calls return the real result;
`list_windows` / `find_window` skip DWM-cloaked and zero-area windows;
`post_key` sends one `WM_CHAR` for a printable character (pass the integer
virtual-key code to get key messages) and `post_key_to_window` accepts
`"enter"` / `"esc"`; `save_window_layout` stores the rectangle `MoveWindow`
positions (re-save older layout files); snap / grid / cascade use the work
area; `wait_for_window` never sleeps past its timeout.
- Screenshots of a region on Windows (`pil_screenshot`, `screenshot`,
`AC_screenshot`, `keyword_screenshot`, `capture_window`) capture any
monitor instead of returning black outside the primary one; a region
entirely off every monitor raises `AutoControlScreenException`.
`grab_logical` clips a region to the desktop and returns the clipped
origin, and set-of-marks renders the whole virtual desktop.
- macOS: `click_mouse(clicks=n)` sets the click-state field on the n-th
click's events, `list_windows` includes minimised windows and `restore`
finds them, and `grab_logical` works in points across every display. None
of this has been run on a Mac yet (`Progress.md`).
- With an Ed25519 key configured, a legacy HMAC signature is refused unless
`JE_AUTOCONTROL_ACCEPT_LEGACY_ACTION_SIGNATURES` is set, and an endpoint
holding only the public key refuses `AC_sign_action_file`.
- A remote-desktop panel reads, drives and disconnects only the session it
opened; a panel whose session is replaced closes its window. Starting a
host no longer disconnects a viewer.
- The Anthropic agent backends no longer rewrite turns they have sent: past
three screenshots the conversation restarts from a summary of the goal and
the actions so far plus the latest screenshot.
- The `pytest11` entry point is the top-level module `je_auto_control_pytest`,
which imports only pytest; `je_auto_control.utils.pytest_plugin` re-exports
it. Reinstall for the new entry point to take effect.
- `cryptography>=50.0.0` (was `>=48.0.1`). There is no `macosx_10_9_x86_64`
wheel at that floor, so an Intel Mac builds it from source.
- Image and OCR: templates load from non-ASCII paths and may be grayscale; an
unreadable or unsupported template raises `ImageNotFoundException`;
`find_spans` finds a phrase that starts inside a long box; the centre of a
box with negative coordinates is floored.
- `keyboard_layout` reports `None` for a Shift half that prints no single
character and labels ISO / UK / ABNT keys; a clipboard format named `None`
normalises to `""`.
- GUI tabs are built the first time they are opened. `AutoControlGUIWidget`
registers all 48 tabs from `gui/tab_registry.py` but constructs only the
three it opens on and its own forms; `list_registered_tabs()` builds
nothing, and reading `entry.widget` on a `_tab_entries` row builds that tab.
An embedder that relied on every tab existing after construction (a tab's
timer or listener started at start-up) has to open the tab first.
- The main window no longer imports `qt_material`; it styles itself from
`gui/theme.py`. The `[gui]` extra still lists `qt-material` for now.
- GUI text size "Auto" is 10 / 11 / 13 pt by screen height (was 12 / 14 / 16),
and the default window is 1280×800 (was 1000×760). The font family is the
platform's UI font instead of Lato.
- `je_auto_control_dev`, the dev-channel package, declares what
`je_auto_control` declares: the same pinned dependencies and platform
markers (`defusedxml`, `cryptography` and the `opencv-python` bound are new
Expand Down
20 changes: 13 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@ python -m build # build
```

**Coverage is measured with `coverage run -m pytest`, never `pytest --cov`.**
This package registers a `pytest11` entry point, so pytest imports
`je_auto_control.utils.pytest_plugin.plugin` — and with it the whole facade —
while loading plugins, before pytest-cov starts. Several hundred modules then
have their import-time lines recorded as never executed: measured, that is
11,962 statements and ~24 percentage points (52.22% vs 72.05% on the same
suite). `test/unit_test/headless/test_coverage_measurement.py` holds CI to the
correct spelling.
This package registers a `pytest11` entry point. While it pointed at
`je_auto_control.utils.pytest_plugin.plugin`, pytest imported that submodule —
and with it the whole facade — while loading plugins, before pytest-cov starts.
Several hundred modules then had their import-time lines recorded as never
executed: measured, that was 11,962 statements and ~24 percentage points
(52.22% vs 72.05% on the same suite). The entry point is now the top-level
module `je_auto_control_pytest`, which imports only pytest
(`test_pytest_entrypoint_light.py` keeps it so), but `coverage run` remains the
rule: it does not depend on what a plugin imports or on which build is
installed, and `pytest --cov` has not been re-measured since.
`test/unit_test/headless/test_coverage_measurement.py` holds CI to the
correct spelling. After changing the entry point, reinstall (`pip install -e .`)
for it to take effect.

**Measure it with the `[webrtc]` extra installed**, which is why it is in the
line above. Eleven modules under `utils/remote_desktop` raise `ImportError` at
Expand Down
Loading
Loading