Skip to content

OMC version 5.3

Choose a tag to compare

@abra-code abra-code released this 15 Sep 22:50
· 18 commits to master since this release

OMC 5 requires macOS 14.6 or later. For older macOS versions, use OMC 4.x.

OMC 5.3 is built against ActionUI 0.8.1. It adds a two-way bridge between a command handler and the ActionUI window it was dispatched from: for the first time a handler can read what is on screen right now, not only write to it. Python handlers get this through a new omc module that AppletBuilder installs for them. Also in this release: embedded-Python thinning built into AppletBuilder's Build & Run pane and the appletbuilder command-line tool, off-screen ActionUI JSON preview rendering that works on a locked screen, and an omctest harness that can test bridge handlers against a real host.

Highlights

Command handlers can read their ActionUI window

omc_dialog_control has always been able to set a value, fill a table or enable a control, but it has never been able to ask what a control currently holds. A handler's only view of its window was the snapshot the engine took at dispatch time - $OMC_ACTIONUI_VIEW_101_VALUE and its siblings - so anything the user changed afterward was invisible.

An applet with an ActionUI window now also serves the ActionUI remote protocol (JSON-RPC 2.0 over a Unix domain socket, from ActionUI 0.8). The engine starts the server with the first ActionUI window and stops it when the applet terminates. Closing the last window deliberately does not stop it, because a script the applet spawned may still be holding the endpoint.

Handlers find the bridge through the environment, under both OMC-prefixed and plain names, so a client written against the ActionUI protocol does not need to know its host is OMC:

  • OMC_ACTIONUI_REMOTE_ENDPOINT - a new always-exported special word - and ACTIONUI_REMOTE_ENDPOINT, merged into every child's environment in every execution mode.
  • ACTIONUI_WINDOW_UUID, the plain-name alias of OMC_ACTIONUI_WINDOW_UUID.
  • ACTIONUI_REMOTE_TOKEN_FD, the descriptor the handler's access token arrives on (see Security below).

The full reference is the new omc_python_bridge_guide.md; omc_runtime_context_reference.md lists the new variables.

The omc Python module

A Python handler can now simply import omc:

import omc

win = omc.window()
name = win.get_string(101)          # what is in the field right now
rows = win.get_rows(5)              # every row of the table, right now
  • omc.window() returns the dispatching window as an omc.OMCWindow, a subclass of ActionUI's actionui_remote.Window, so every ActionUI verb - reading, writing, tables, adding and removing elements, modals and toasts, batching - travels over the bridge. It raises omc.OMCError with a specific message when the applet serves no bridge (a command file with no ACTIONUI_WINDOW) or the command was not dispatched from a window.
  • OMC's own window verbs - terminate, select, set command id, title, resize, move and next command - call the existing omc_dialog_control and omc_next_command tools with exactly the arguments a shell handler would use, so each verb still has a single implementation.
  • omc.context() parses the runtime environment the engine exports, including the dispatch-time value snapshots, into named fields, so a handler no longer opens with a block of os.environ.get calls.

Nothing to install. AppletBuilder copies omc.py and actionui_remote.py into Contents/Library/Packages/ of every applet that has Python handlers and an embedded Python runtime, both when building and when creating an applet from a template. That directory is already on PYTHONPATH and is never touched by a Python runtime update, so the modules survive one. An applet that uses the system Python gets nothing, because the engine does not put that directory on PYTHONPATH for it.

AppletBuilder also vendors ActionUI's shell clients - actionui_remote.sh and actionui_remote.zsh, together with the two awk programs they load, actionui_remote_escape.awk and actionui_remote_walk.awk - and copies them into built applets. The install is all-or-nothing: a partial copy is rolled back, since three of the four files make a client that refuses to load.

Embedded Python thinning from the Build & Run pane

The Build & Run pane gains an Embedded Python Thinning group, and the appletbuilder agent command-line tool gains a matching appletbuilder thin-python plan|apply|plan-apply subcommand. Both drive one shared phase in lib.build.sh, the way Build and Test already do, and AppletBuilder now bundles the Python-Embedding thinning toolkit under Contents/Library/python_thinning, so neither needs a checkout of that repository.

The workflow is unchanged: write a reviewable JSON plan from a clone of the applet, then apply it, which removes what the plan names, verifies the result, and restores the interpreter if anything needed went with it. On a fresh Python applet that takes the interpreter from 61.3 MB to 43.1 MB.

Thinning deliberately stays out of Build, and every path that removes modules asks for confirmation first, naming the bundle. When no plan sits beside the bundle, apply falls back to the last plan written for that applet, matched on its bundle identifier, and says so in the log. That supports the intended release flow: plan against the development copy, apply to a distribution copy. An optional per-applet <App>.thinning-keep.txt lists modules to keep regardless of the analysis.

Off-screen preview that works on a locked screen

appletbuilder preview now runs the bundled ActionUIViewer with --hide-window, which draws the window in-process at an off-screen position. No window appears over the desktop, no Screen Recording permission is involved, and the capture works while the screen is locked - the usual state in an agent session, where the old window-server capture returned an empty image. Pass --show-window for the previous behavior.

Known gaps of the off-screen capture, from ActionUI 0.8.1: no window shadow, TabView tab segments draw as a solid block, and a locked screen gives controls the inactive gray tint.

The skill gains a hard rule telling an agent to screenshot an ActionUI change and look at the image, and the nib migration guide drops its "transparent or black on a locked screen" trap.

Security

The bridge token is handed to each handler on a descriptor, never in the environment

The ActionUI bridge refuses clients that do not present a token. A handler's environment is copied from the applet's, and a process's exec-time environment is visible to any process of the same user through ps -E - python3 and node cannot be made to hide it. A token passed in the environment would therefore have been published machine-wide for as long as the handler ran, and clearing the variable inside the handler does not help, because ps reads a snapshot taken at exec.

So the engine mints one token per spawned handler and delivers it on an inherited pipe: the token is written before the spawn, the read end becomes descriptor 3 in the child, and the parent closes both ends immediately. The child is told the number through ACTIONUI_REMOTE_TOKEN_FD=3 and never sees ACTIONUI_REMOTE_TOKEN - the host removes that from its own environment as soon as the bridge starts, and the spawn strips it from the child's environment as a second layer. The shipped Python and shell clients read the token from the descriptor.

Known limits, documented and pinned by tests:

  • Grants live until the applet terminates. A handler may background work that needs the bridge later, and nothing reliably knows when everything a command spawned has finished.
  • exe_terminal and exe_iterm get no token: Terminal is not the applet's child and inherits no descriptor, and writing the token into the export script would put it in a file in the clear.
  • exe_silent_system and the AppleScript execution modes get no token either, since they take neither an environment nor a descriptor from the engine. These modes can still change the window through omc_dialog_control, which needs no token.

Nothing regresses: the bridge is new in this release.

Testing

omctest stands up a real bridge host

A Python handler that reads its window needs a host on the other end of the socket, and under test there is no engine. For an applet that ships the client, the harness now runs ActionUI's own fake host for the length of a test file, seeded with the window UUID the harness already exports and with the applet's elements. Without it, such a handler did not fail an assertion - it raised, and the whole file read as a crash.

It is a real host rather than a recording stub: the client and protocol are the shipped ones, so an applet is tested against a genuine implementation.

  • bridge_called and bridge_value are new assertions that read what the applet did over the bridge. They sit beside ui_value on purpose: ActionUI verbs travel over the bridge, while OMC's own window verbs still go through omc_dialog_control - so bridge_value reads back a set_value and ui_value reads back a set_title.
  • omc_window_switch now carries the plain-name window UUID as well as the prefixed one. The client prefers the plain name, so leaving it behind pointed a handler at the window the test had just left.

New and wider test coverage

  • An engine test drives the whole stack at once: the engine dispatches a Python handler, which imports the modules from where AppletBuilder installs them and drives a real ActionUI window over the real bridge, and the test then asks the engine independently whether it holds what the module wrote.
  • New engine tests for the bridge host (OMCActionUIRemoteTests) and the token-carrying spawn (OMCPopenTests). This also fixes five engine tests that had been failing since ActionUI turned on the token requirement.
  • A unit suite for the omc module (Tools/omc_python_tests).
  • AppletBuilder's own omctest suite grows to 471 checks across five files. The new 50-thin-python.test.sh (164 checks) covers every decision AppletBuilder makes around thinning: refusing an applet with no embedded Python, plan placement, the three plan-resolution routes, both answers to the confirmation, plan-then-apply ordering, and the pane's checkbox combinations.

Compatibility notes

  • Rebuild applets with AppletBuilder 5.3 to use the bridge. The server lives in Abracode.framework and the omc module is installed at build time. If import omc raises ModuleNotFoundError, the applet was built by an older AppletBuilder.
  • Applets that do not use the bridge are unaffected. omc_dialog_control and the dispatch-time OMC_ACTIONUI_VIEW_* snapshots work exactly as before; the bridge guide has a section on when to keep using them.
  • OMCTEST_API_VERSION goes from 6 to 7.
  • appletbuilder preview renders off screen by default. Scripts that relied on a visible window should pass --show-window.
  • The applet catalog now lists Watchdog.app as converted to ActionUI, leaving four nib-based applets: Xattr, Delta, Enoch and AIChat.

ActionUI 0.8.1

The bundled ActionUI moves to 0.8.1, which brings:

  • ActionUIRemote (0.8) - the JSON-RPC 2.0 remote protocol over a Unix domain socket, a normative protocol document, the optional token and its descriptor hand-off, a Python client with a fake host for test suites and a command line, shell clients for bash and zsh, and a Node client.
  • ActionUIJSON (0.8) - the JSON encoding shared by every adapter, plus window enumeration.
  • Off-screen capture in ActionUIViewer (0.8.1) - --method offscreen and --hide-window, with the older capture methods falling back to off-screen drawing when the screen is locked or the capture comes back empty.
  • VideoPlayer fix for package builds (0.8.1) - the ActionUI Swift package now links AVKit explicitly. Without it, any document with a VideoPlayer element crashed at load in a Swift Package Manager executable such as ActionUIViewer.

Documentation and skill

  • New Documentation/omc_python_bridge_guide.md, also bundled in AppletBuilder: the two entry points, reading, writing, tables, elements, modals, OMC's own window verbs, batching, errors, when to stay with omc_dialog_control, testing a bridge handler, and how it works underneath.
  • omc_python_scripting_guide.md introduces the module and links to the guide; omc_runtime_context_reference.md documents the bridge variables; omctest_guide.md documents the bridge host and the two new assertions.
  • appletbuilder_user_guide.md covers the thinning group and contrasts the GUI's live Preview window with the command-line tool's off-screen PNG, and the agent README documents thin-python, off-screen preview and --show-window.
  • building_omc_applet.md lists WatchdogApp as an ActionUI applet with Python handlers.
  • The skill lists the bridge variables where an agent looks for what it can reach, adds the bridge guide to its reference table and the new assertions to its testing section, and adds the screenshot rule.
  • omc_applet_catalog.md updated for Watchdog.app.

In this distribution

  • AppletBuilder.app - OMC applet development studio.
  • OnMyCommandCM.plugin - contextual menu plugin for use with Shortcuts.app; commands load from ~/Library/Preferences/com.abracode.OnMyCommandCMPrefs.plist.
  • OMCService.service - macOS service template for standalone OMC-based system services.
  • Skill/ - the OMC AI agent skill (three flavors) plus its installer.
  • Scripts/ - codesign_applet.sh, install_contextual_menu_plugin.sh, thin_distribution.sh, OMCApplet.entitlements, and the example com.abracode.OnMyCommandCMPrefs.plist.

See the main OMC README at https://github.com/abra-code/OMC/ for full documentation on commands, runtime context, dialogs, and services.