apps/companion-mac)Architecture documentation — generated for the ExcelRewind architecture doc suite. Source-verified against the repository at commit range ending bf96420 (2026-07-17). Not marketing copy: every name below is a real file, function, RPC, or endpoint in the repo.
The Mac Companion is a small, local, loopback-only Python HTTP server that runs alongside Microsoft Excel for Mac
and gives the ExcelRewind Excel add-in (apps/addin-excel, an Office.js task pane hosted in WKWebView)
a way to see and briefly control the user's other open workbooks — something Office.js's sandboxed
JavaScript API cannot do on its own. It is the macOS twin of apps/companion-win
(a Windows COM-based service) and is deliberately written so its wire protocol is byte-for-byte identical:
same 5-port range, same endpoint paths, same JSON shapes, same Bearer-token auth. The add-in's
apps/addin-excel/src/companion.ts client works unmodified against either OS.
Where it runs: as a background macOS process with no Dock icon (LSUIElement in the app's Info.plist)
and a small menu-bar item ("ExcelRewind Companion" via rumps) as its only visible UI.
It is designed not to launch Excel — the read paths (list_workbooks(), save_copy(),
spotlight()'s entry check, _find_wb()) explicitly gate their AppleScript on
_excel_or_none() (which shells out to pgrep -x "Microsoft Excel") and no-op if Excel
isn't running. open_ref()/open_refs() and _active_wb_name() do
not re-check before their own tell application "Microsoft Excel" — see the
Invariants section for why this is a real gap in the guarantee, not just a documentation nuance. It talks to
Excel exclusively through AppleScript (shelling out to osascript), because there
is no macOS COM equivalent; this replaces the Windows companion's win32com/COM automation of Excel.
Build/deploy shape: it's a plain Python package (src/companion/, no native extension) run from source
during development (python run_companion.py), and packaged for distribution with py2app
(setup.py, invoked by build.sh) into a universal (arm64+Intel) .app bundle when
built with the system Python 3. For end users it ships as a single double-clickable .pkg
installer (installer/build-installer.sh → installer/dist/ExcelRewind-Installer.pkg), whose
postinstall script (installer/scripts/postinstall) installs the app to /Applications,
sideloads the add-in manifest into Excel's wef folder, trusts the companion's self-signed loopback TLS
certificate in the System keychain, and registers a per-user launchd LaunchAgent so the companion
auto-starts at login. The build is currently unsigned — code-signing/notarization with an Apple
Developer ID is an owner-side task not yet done (README §"Notarization / signing").
Current scope is a deliberate, owner-decided cut: "spotlight/provenance first." Pairing, /status,
/workspace, /file, /open, /open-many, the Provenance Spotlight
(/spotlight, /spotlight/clear), heartbeat, and shutdown are all fully real and
mac-verified against a live Excel 365 for Mac session (2026-07-14 through 2026-07-15). Tier-2
record-all-workbooks is deferred — /record/start, /record/events, and
/record/stop are graceful no-op stubs that return the exact response shapes the add-in expects, so the
add-in never errors against a Mac companion; it simply never receives cross-workbook change events on this platform yet.
Internal components, grouped by responsibility. Solid arrows are direct calls; the HTTP boundary (external, to the add-in pane) is drawn at the top.
flowchart TB
PANE["Excel add-in pane
(WKWebView, companion.ts)"]
subgraph ENTRY["Entry / process lifecycle"]
RUN["run_companion.py
(source + py2app entry point)"]
MAIN["main.py: main()
single-instance guard, wiring, signal handling"]
end
subgraph HTTP["HTTP layer — server.py"]
BIND["bind_server() / serve()
ThreadingHTTPServer, ports 38743-38747"]
COMP["class Companion
(shared state: paired, last_seen, spotlight flags)"]
HANDLER["make_handler() -> Handler
(BaseHTTPRequestHandler routes)"]
WATCHDOG["_watchdog()
2s tick -> check_idle()"]
end
subgraph SEC["Security"]
AUTH["auth.py: class Auth
token load/create, pair(), check()"]
DIALOG["confirm_pairing_dialog()
osascript display dialog"]
TLSMOD["tls.py
ensure_cert(), wrap_socket()"]
end
subgraph WORK["Worker thread — worker.py"]
WORKER["class Worker
submit(fn) -> queue -> 8s timeout -> ExcelBusy"]
end
subgraph BACKEND["Excel backend — excel_applescript.py"]
BACKENDCLS["class ExcelAppleScript"]
OSASCRIPT["_run_osascript()
subprocess -> osascript -e"]
SPOT["spotlight() / clear_spotlight()
_activate() / _activate_script()"]
CROSS["_apply_cross() / _remove_cross()
focus-cell cross-hair CF"]
REFOPS["open_ref() / open_refs() / save_copy()"]
STUBS["start_record() / poll_events() / stop_record()
(wired but unused — Tier-2 deferred)"]
end
subgraph EVT["events.py (buffers, unused in this build)"]
EVTBUF["EventBuffer
coalesce + drop-oldest"]
SPANBUF["SpanBuffer
focus-span capture"]
end
subgraph TRAY["tray_mac.py"]
TRAYAPP["CompanionApp(rumps.App)
menu-bar Status / Quit"]
end
subgraph EXCEL["Microsoft Excel for Mac (external app)"]
EXCELAPP["Excel process
(Apple Events target)"]
end
PANE -- "HTTPS loopback JSON
Bearer token" --> BIND
BIND --> HANDLER
HANDLER --> COMP
HANDLER -- "unauthed: /pair only
(/status never touches Auth)" --> AUTH
AUTH --> DIALOG
HANDLER -- "authed calls" --> WORKER
WORKER --> BACKENDCLS
BACKENDCLS --> OSASCRIPT
OSASCRIPT -- "osascript -e ..." --> EXCELAPP
BACKENDCLS --> SPOT
BACKENDCLS --> CROSS
BACKENDCLS --> REFOPS
BACKENDCLS --> STUBS
STUBS --> EVTBUF
STUBS --> SPANBUF
WATCHDOG -- "companion.check_idle()" --> COMP
WATCHDOG --> WORKER
RUN --> MAIN
MAIN --> BIND
MAIN --> TLSMOD
TLSMOD -- "wraps httpd.socket" --> BIND
MAIN --> TRAYAPP
TRAYAPP -- "reads status via worker.submit" --> WORKER
The companion never writes a .rewind file itself and never touches the user's original spreadsheet content
in bulk — it moves small, purpose-specific payloads: JSON status/control messages, base64-encoded workbook copies
(never the live original), and short AppleScript control scripts. Two flows matter most: (a) pulling a reference
workbook's bytes out of Excel for cross-workbook resolution, and (b) pushing focus/selection commands back into Excel
for the Provenance Spotlight.
flowchart LR
subgraph ADDIN["apps/addin-excel (pane)"]
CTS["companion.ts client"]
REWIND[".rewind assembly
(packages/format, pane-side — NOT this module)"]
end
subgraph MAC["companion-mac"]
EP["HTTP endpoints (server.py)"]
WK["Worker (worker.py)"]
BE["ExcelAppleScript (excel_applescript.py)"]
DISK["Local files:
~/Library/Application Support/ExcelRewind/
token.txt, tls/*.pem, companion-port.txt
tempdir ExcelRewind/refs (opened ref copies)
~/Library/Containers/com.microsoft.Excel/Data/tmp (save_copy throwaway)"]
end
subgraph EXCEL["Microsoft Excel for Mac"]
WB1["Primary / host workbook"]
WB2["Reference workbook(s)"]
end
CTS -- "GET /workspace (Bearer)" --> EP
EP -- "list_workbooks()
AppleScript: name/full name/saved per workbook" --> BE
BE -- "osascript" --> WB1
BE -- "osascript" --> WB2
EP -- "JSON [{name,fullName,saved}]" --> CTS
CTS -- "POST /open {name,data_b64}" --> EP
EP -- "base64 decode -> bytes" --> WK
WK -- "open_ref(name,data)" --> BE
BE -- "write bytes to REF_DIR/<name>" --> DISK
BE -- "osascript: open workbook ... read only true" --> WB2
EP -- "{opened,alreadyOpen}" --> CTS
CTS -- "GET /file?name=Book2.xlsx" --> EP
EP -- "save_copy(name)" --> BE
BE -- "fast path: read saved file bytes from disk
OR live path: copy worksheet -> throwaway -> save workbook as (sandbox tmp) -> read -> delete" --> DISK
EP -- "application/octet-stream bytes" --> CTS
CTS --> REWIND
CTS -- "POST /spotlight {name,sheet,range}" --> EP
EP -- "set_spotlight -> worker.submit(spotlight)" --> WK
WK -- "spotlight() -> _activate() -> _activate_script()" --> BE
BE -- "osascript: activate, select range, cross-hair CF" --> WB2
EP -- "{spotlighted:true, restore:<prevWbName>}" --> CTS
CTS -- "POST /spotlight/clear" --> EP
EP -- "clear_spotlight()" --> BE
BE -- "osascript: activate restore target, delete cross CF" --> WB1
sequenceDiagram
participant Pane as "Add-in pane (companion.ts)"
participant Srv as "server.py Handler"
participant Auth as "auth.py Auth"
participant OS as "osascript (display dialog)"
participant User as "macOS user"
Pane->>Srv: GET /status (no auth)
Srv->>Srv: excel_detected() via worker
Srv-->>Pane: 200 {product, version, excelDetected}
Pane->>Srv: POST /pair (no auth)
Srv->>Auth: auth.pair()
alt a dialog is already open
Auth-->>Srv: raise PairingBusy
Srv-->>Pane: 409 {error: pairing-in-progress}
else no dialog open
Auth->>OS: osascript -e 'display dialog ... buttons {Deny, Allow}'
OS->>User: "Allow the ExcelRewind add-in to connect to the Companion?"
alt user clicks Allow
User-->>OS: button returned:Allow
OS-->>Auth: returncode 0, stdout contains "Allow"
Auth-->>Srv: token (32 hex chars)
Srv->>Srv: companion.mark_seen() (paired=true)
Srv-->>Pane: 200 {token}
else user clicks Deny / dialog times out (120s)
OS-->>Auth: returncode != 0 or no "Allow"
Auth-->>Srv: None
Srv-->>Pane: 403 {error: pairing-declined}
end
end
Note over Pane,Srv: Every subsequent call sends
Authorization: Bearer <token>
checked with secrets.compare_digest
sequenceDiagram
participant Pane as "Add-in pane"
participant Srv as "server.py"
participant Wk as "worker.py Worker"
participant BE as "ExcelAppleScript"
participant Excel as "Microsoft Excel for Mac"
Pane->>Srv: POST /open-many {files:[{name,data_b64}, ...]}
Srv->>Wk: submit(open_refs(items))
Wk->>BE: open_refs(items)
BE->>BE: host = _active_wb_name() (capture pre-open active workbook)
loop each ref file
BE->>BE: write bytes to REF_DIR/<safe name>
BE->>Excel: osascript: ask to update links -> false, open workbook ... read only true, restore alert/link settings
Excel-->>BE: workbook opened (or link-dialog suppressed)
BE->>BE: _maximize(name)
end
BE->>Excel: osascript: activate object <host> (restore focus after batch)
BE-->>Wk: [{name,opened,alreadyOpen}, ...]
Wk-->>Srv: results
Srv-->>Pane: 200 {results:[...]}
Pane->>Srv: POST /spotlight {name,sheet,range}
Srv->>Wk: submit(spotlight(name,sheet,rng))
Wk->>BE: spotlight(name, sheet, rng)
BE->>BE: prev = _active_wb_name(), anchor _spotlight_prev (only if not already anchored)
BE->>Excel: osascript _activate_script: activate, activate object workbook, activate object sheet,
delay 0.4s, select cross-hair sweep, delay 0.35s, select target_range, activate object target_range
Excel-->>BE: workbook/sheet/range now frontmost + selected
BE->>BE: _apply_cross(wb,sheet,rng) — only if wb is a companion-opened REF_DIR file
BE->>Excel: osascript: make new format condition (formula =OR(ROW()=r,COLUMN()=c))
BE-->>Wk: restore target name
Wk-->>Srv: restore
Srv-->>Pane: 200 {spotlighted:true, restore}
Note over Pane,Srv: repeat POST /spotlight for each cue in a streak,
_spotlight_prev is NOT overwritten while still anchored,
so a multi-cue streak always restores to the true origin
Pane->>Srv: POST /spotlight/clear
Srv->>Wk: submit(clear_spotlight())
Wk->>BE: clear_spotlight()
BE->>Excel: osascript: delete cross-hair format condition (signature-guarded)
BE->>Excel: osascript: activate object <_spotlight_prev>
BE-->>Wk: restored name (or None if origin workbook was closed)
Wk-->>Srv: restored
Srv-->>Pane: 200 {cleared:true, restored}
Note over Srv: Watchdog (_watchdog, 2s tick) auto-fires clear_spotlight
if the pane never clears within SPOTLIGHT_IDLE_TIMEOUT=15s
save_copy fast vs. live path
sequenceDiagram
participant Pane as "Add-in pane"
participant Srv as "server.py"
participant BE as "ExcelAppleScript"
participant FS as "Disk"
participant Excel as "Microsoft Excel for Mac"
Pane->>Srv: GET /file?name=Book2.xlsx
Srv->>BE: save_copy("Book2.xlsx") (via worker)
BE->>BE: wb = _find_wb(name) — by-index workbook loop (Mac cannot use "repeat with w in workbooks")
alt workbook not open
BE-->>Srv: raise FileNotFoundError
Srv-->>Pane: 404 {reason: not-open}
else workbook found and saved==true (fast path)
BE->>FS: read bytes directly from the workbook's on-disk path
FS-->>BE: bytes
BE-->>Srv: (data, filename)
Srv-->>Pane: 200 octet-stream (exact on-disk bytes)
else workbook unsaved / has pending changes (live path)
BE->>Excel: osascript: copy worksheet (sheet 1) -> new throwaway workbook,
loop copy worksheet 2..N after last sheet of throwaway
BE->>Excel: osascript: save workbook as <throwaway> filename <EXCEL_CONTAINER_TMP/...>
Excel-->>BE: throwaway saved inside Excel's sandbox container
BE->>Excel: osascript: close workbook <throwaway> saving no
BE->>FS: read throwaway bytes, then delete throwaway + Excel's "~$" owner file
alt live path fails mid-way
BE->>Excel: best-effort: close throwaway workbook saving no
BE-->>Srv: re-raise original exception
Srv-->>Pane: 500 {reason: save-failed, detail}
else success
BE-->>Srv: (data, filename)
Srv-->>Pane: 200 octet-stream
end
end
Note over BE: chart sheets + VBA are NOT carried by "copy worksheet"
(values/formulas/formats only) — acceptable for ref resolution,
not full-fidelity duplication
.pkg installer
sequenceDiagram
participant User as "End user"
participant Installer as "macOS Installer.app"
participant Post as "installer/scripts/postinstall (root)"
participant AsUser as "console user (sudo -u)"
participant KC as "System Keychain"
participant LD as "launchd"
participant App as "ExcelRewind Companion.app"
User->>Installer: double-click ExcelRewind-Installer.pkg -> Continue -> enter admin password
Installer->>Post: run postinstall script (as root, $3=target volume)
Post->>Post: CONSOLE_USER = stat -f%Su /dev/console (identify the real logged-in user)
Post->>AsUser: mkdir wef folder, cp rewind-manifest.xml into
~/Library/Containers/com.microsoft.Excel/Data/Documents/wef
Note right of AsUser: sideloads the add-in manifest — Excel now shows
ExcelRewind under Insert -> My Add-ins -> Developer Add-ins
Post->>LD: launchctl bootout gui/<uid>/com.excelrewind.companion (stop any prior instance — idempotent upgrade)
Post->>AsUser: pkill -TERM "ExcelRewind Companion.app/Contents/MacOS" (graceful stop, SIGTERM-safe since 0.1.1)
Post->>AsUser: APP_BIN --ensure-cert (as the console user)
AsUser->>App: tls.ensure_cert() generates companion-cert.pem / companion-key.pem if missing (0600, ~/Library/Application Support/ExcelRewind/tls)
App-->>Post: prints cert path to stdout
Post->>KC: security add-trusted-cert -d -r trustRoot -p ssl -k /Library/Keychains/System.keychain <cert>
KC-->>Post: cert trusted for SSL, scoped to 127.0.0.1/localhost only
Post->>AsUser: write ~/Library/LaunchAgents/com.excelrewind.companion.plist (RunAtLoad=true, KeepAlive=false)
Post->>LD: launchctl bootstrap gui/<uid> <plist>
alt bootstrap fails (already bootstrapped / older macOS)
Post->>LD: launchctl kickstart gui/<uid>/com.excelrewind.companion
Post->>AsUser: fallback: open "ExcelRewind Companion.app"
end
LD->>App: launch ExcelRewind Companion.app/Contents/MacOS/ExcelRewind Companion
App->>App: main() single-instance check, bind_server, tls.wrap_socket, start tray (rumps, main thread)
Note over App: On the FIRST AppleScript call to Excel, macOS separately prompts
"ExcelRewind Companion wants to control Microsoft Excel" (Automation/TCC) — the user must click OK
sequenceDiagram
participant Pane as "Add-in pane"
participant Srv as "server.py"
participant Wk as "Worker"
participant BE as "ExcelAppleScript"
participant Page as "Malicious/other web page"
Pane->>Srv: POST /pair
Note right of Srv: a second /pair arrives while the first dialog is still open
Pane->>Srv: POST /pair (concurrent)
Srv->>Srv: auth.pair() -> _pair_lock.acquire(blocking=False) fails
Srv-->>Pane: 409 {error: pairing-in-progress}
Pane->>Srv: POST /open {name,data_b64}
Srv->>Wk: submit(open_ref)
Wk->>BE: osascript open workbook ...
Note right of BE: Excel is showing a modal (e.g. an unrelated dialog) and osascript hangs
Note right of Wk: Worker.submit()'s own wait is a FIXED 8s (set once in main.py's
Worker(backend)) - independent of open_ref's own 30s osascript timeout,
so the 8s caller-facing timeout fires FIRST no matter which endpoint this is
Wk--xSrv: box.wait(8.0) elapses -> raise ExcelBusy
Srv-->>Pane: 503 {reason: excel-busy}
Note over BE: the worker thread is still stuck inside osascript for up to ~22
more seconds (open_ref's internal timeout=30s) - a second request queues
behind it and can ALSO 503 at its own +8s before the first ever finishes
BE--xWk: subprocess.TimeoutExpired at 30s (open_ref) / 8s (default elsewhere) - too late, caller already got its 503
Page->>Srv: POST /shutdown (cross-origin fetch, Origin: https://evil.example)
Srv->>Srv: origin not in ALLOWED_ORIGINS
Srv-->>Page: 403 {error: forbidden-origin}
Note over Srv: same-machine curl/CLI/uninstaller send NO Origin header -> allowed through unchanged
The companion is intentionally isolated: it has no network calls beyond 127.0.0.1 and never talks to Supabase, Stripe, or any ExcelRewind backend directly. All of its external edges are either the local Excel application (via AppleScript) or local OS services (Keychain, launchd, filesystem).
flowchart LR
ADDIN["apps/addin-excel
(Office.js pane, WKWebView)"]
MAC["apps/companion-mac"]
EXCELAPP["Microsoft Excel for Mac
(external application)"]
KEYCHAIN["macOS System Keychain"]
LAUNCHD["macOS launchd
(per-user LaunchAgent)"]
FS["Local filesystem
~/Library/Application Support/ExcelRewind
+ Excel sandbox container tmp"]
INSTALLER["apps/companion-mac/installer
(.pkg build + postinstall)"]
ADDIN <-- "HTTPS loopback, JSON,
Bearer token, ports 38743-38747" --> MAC
MAC -- "osascript / AppleScript
(Apple Events, TCC-gated)" --> EXCELAPP
MAC -- "read/write token.txt, tls/*.pem,
companion-port.txt, refs/*" --> FS
INSTALLER -- "security add-trusted-cert
(root, SSL-only, 127.0.0.1/localhost SAN)" --> KEYCHAIN
INSTALLER -- "sideloads rewind-manifest.xml
into Excel's wef folder" --> EXCELAPP
INSTALLER -- "writes + bootstraps LaunchAgent plist" --> LAUNCHD
LAUNCHD -- "RunAtLoad, auto-start" --> MAC
| Peer | Direction | Protocol / mechanism | Purpose |
|---|---|---|---|
apps/addin-excel pane (companion.ts) | in + out | HTTPS loopback REST, JSON bodies, Authorization: Bearer <token> on all but /status//pair; CORS locked to https://addin.excelrewind.com and https://localhost:3000 | Pairing, workspace listing, cross-workbook ref open/fetch, Provenance Spotlight control, heartbeat, clean shutdown |
| Microsoft Excel for Mac (application) | out only | osascript -e <script> subprocess per call — Apple Events; requires the user to grant Automation ("control Microsoft Excel") permission on first use | List open workbooks, save/copy workbook bytes, open reference workbooks, activate/select ranges for spotlighting, apply/remove the focus-cell cross-hair conditional format |
| macOS System Keychain | out (installer only) | security add-trusted-cert -d -r trustRoot -p ssl -k /Library/Keychains/System.keychain, run as root by the pkg postinstall | Trust the companion's self-signed loopback TLS cert (SAN-bound to 127.0.0.1/localhost) so WKWebView's mixed-content check allows the pane's https fetch to the companion |
macOS launchd | out (installer + main.py signal handling) | Per-user LaunchAgent plist (~/Library/LaunchAgents/com.excelrewind.companion.plist), launchctl bootstrap/bootout/kickstart; the running process registers SIGTERM/SIGINT handlers for clean bootout | Auto-start the companion at login; clean stop/restart on upgrade or uninstall |
| Local filesystem | in + out | Plain file I/O under ~/Library/Application Support/ExcelRewind/ (token, TLS keypair, port marker), tempdir()/ExcelRewindCompanion.lock (single-instance PID lock), tempdir()/ExcelRewind/refs (opened reference copies), ~/Library/Containers/com.microsoft.Excel/Data/tmp (sandbox-writable throwaway for save_copy's live path) | Persist pairing token + TLS cert across restarts, discover the live port, stage reference workbook bytes for Excel to open, work around Excel's App Sandbox write restriction |
apps/addin-excel/public/manifest.prod.xml | in (build-time, installer only) | File copy: installer/build-installer.sh packages this file as rewind-manifest.xml into the pkg's Scripts payload; postinstall copies it into Excel's wef folder | Sideload the Excel add-in with zero Terminal steps as part of the same installer that installs the companion |
| Supabase / Stripe / any ExcelRewind backend | none | — | Not contacted. The companion has no outbound network calls beyond 127.0.0.1; all auth/billing/licence logic lives in the pane and backend, never here. |
| File | Function / export | What it does | Why it matters |
|---|---|---|---|
run_companion.py | module entry | Inserts src/ onto sys.path then calls companion.main.main(); used both as python run_companion.py and as py2app's APP target | Same file must work unfrozen (dev) and frozen (packaged) — the sys.path shim is what makes that true |
src/companion/main.py | main() | Single-instance guard (port probe + PID lockfile), wires ExcelAppleScript+Worker+Auth into a Companion, binds the server, wraps it in TLS if a cert exists, writes the port marker file, installs signal handlers, then blocks on tray_mac.run() | The whole process's composition root; also the only place that decides http vs. https at runtime |
main.py | _another_companion_running() | Probes all 5 ports over both https and http for a live /status containing "excelrewind-companion" before even trying the PID lockfile | Catches a second launch even if it predates the lockfile guard (upgrade-safety belt-and-braces) |
main.py | _on_signal() | SIGTERM/SIGINT handler that tears down then calls os._exit(0) | Without the explicit exit, rumps's NSApplication run loop survives the handler and the process lingers until SIGKILL — observed live 2026-07-14; launchctl bootout depends on this for clean upgrades |
src/companion/server.py | class Companion | Shared server state: worker/auth handles, current TLS scheme, paired/connected flags, spotlight-active timestamp | Single source of truth the tray's Status dialog and the watchdog both read |
server.py | Handler._handle_shutdown() | Rejects POST /shutdown if it carries an Origin header not in ALLOWED_ORIGINS; otherwise responds 200 first, tears down 0.3s later on a daemon thread | The only CSRF-relevant endpoint (no-auth by design, since a local tool needs to be killable); this is the guard against a malicious page cross-origin-POSTing it |
server.py | _handle_record_start_stub / _handle_events_stub / _handle_record_stop_stub | Return the exact response shapes companion-win's real endpoints return, but do nothing and record nothing | Tier-2 boundary: this is precisely where "record-all-workbooks" would get wired up when built — the add-in already tolerates it being a no-op |
src/companion/worker.py | class Worker, submit(fn) | One dedicated thread pulls (fn, box) off a queue and runs fn(backend); caller blocks up to timeout (default 8s) then gets ExcelBusy | Serializes every AppleScript call so concurrent osascript processes never race each other; gives server.py an OS-agnostic contract identical to Windows' COM/STA worker |
src/companion/auth.py | _load_or_create_token(), Auth.check() | 32-hex-char token persisted at ~/Library/Application Support/ExcelRewind/token.txt; compared with secrets.compare_digest | Constant-time compare avoids a timing side-channel on the loopback bearer token |
auth.py | confirm_pairing_dialog() | osascript -e 'display dialog ... buttons {"Deny","Allow"}', 120s timeout; refuses (rather than silently granting) if osascript is missing or the call errors | The macOS-native equivalent of Windows' MessageBoxW pairing consent — fail-closed by design |
src/companion/excel_applescript.py | ExcelAppleScript.list_workbooks() | Iterates workbooks by index (repeat with i from 1 to (count of workbooks)) | Documented mac-verified gotcha: repeat with w in workbooks and in (every workbook) both fail with AppleScript error -50 on Excel for Mac |
excel_applescript.py | save_copy(name) | Fast path reads on-disk bytes directly for a saved workbook; live path copies every worksheet into a throwaway workbook and saves that into EXCEL_CONTAINER_TMP (Excel's own sandbox-writable tmp) | Mac has no SaveCopyAs; save workbook as on the real workbook has SaveAs semantics (would retarget/corrupt the open file) and the App Sandbox rejects writes outside Excel's container — this is the workaround for both |
excel_applescript.py | open_ref(name, data) | Writes bytes under REF_DIR/<safe name>, opens via open workbook workbook file name <path> read only true, then re-verifies the workbook actually appeared | Plain open "path" intermittently exits 0 while opening nothing (mac-verified); the re-verify step turns that silent failure into a real error instead of a phantom success |
excel_applescript.py | open_ref same-name collision check | If a workbook of the same name is already open, distinguishes "our real ref" from an unsaved blank workbook (e.g. default Book1) by checking for a disk path via _posixify | Field bug fixed 2026-07-15: treating a blank Book1 as "ref already open" silently spotlit an empty workbook |
excel_applescript.py | open_refs(items) | Captures the active workbook before opening any refs, opens each (per-file error isolation), then re-activates the original host | Without the restore, the last-opened ref stayed focused and the first spotlight lost its restore anchor (field bug 2026-07-15) |
excel_applescript.py | _activate_script(wb_name, sheet, rng) | Pure function building the AppleScript for the Provenance Spotlight; uses the select verb (not set selection to) because the latter is deferred/non-synchronous on Excel-for-Mac and races the next apple-event | Root-caused, agent-proven field bug (2026-07-15): range cues like $B:$J silently never showed because the prior "set selection to" line hadn't committed before the next script line ran |
excel_applescript.py | spotlight(name, sheet, rng) | Anchors _spotlight_prev only while still pointing at the workbook being left, so a streak of cues without an intervening clear always restores to the true pre-streak origin | Multi-cue Tier-2-style replays depend on this not clobbering the restore target on the second/third cue |
excel_applescript.py | _apply_cross / _remove_cross / _is_companion_ref | Adds/removes a conditional-format "cross-hair" (=OR(ROW()=r,COLUMN()=c)) on the spotlighted sheet, ONLY when the workbook is one the companion itself opened from REF_DIR; removal is guarded by checking the formula prefix before deleting | Direct enforcement of "never modify the original spreadsheet" at the code level — the safety check is path.startswith(REF_DIR + os.sep), and deletion won't touch a conditional format the source file shipped with |
excel_applescript.py | _posixify(p) | Pure: maps Excel's full name string to a POSIX path, or None if the workbook was never saved | The load-bearing primitive behind both the blank-workbook collision guard and the fast-vs-live save_copy path decision; unit-tested without osascript |
src/companion/tls.py | ensure_cert(), wrap_socket(httpd) | Generates a per-machine self-signed leaf (SAN = 127.0.0.1 + localhost, RSA-2048, 800-day validity) via the system openssl, then wraps the bound HTTP server's socket in TLS | Existence of this module IS the fix for a whole class of "pane can't see companion" reports on Mac — see §7 |
src/companion/tray_mac.py | run(companion, port, on_quit) | Builds a rumps.App with Status/Quit menu items; starts a 1s rumps.Timer heartbeat purely so pending Python signal handlers get a chance to run | rumps blocks the main thread inside the ObjC run loop, during which CPython executes no main-thread bytecode — without the heartbeat, SIGTERM never actually triggers Python's handler (observed live 2026-07-14) |
src/companion/events.py | EventBuffer, SpanBuffer | Coalescing event buffer and focus-span buffer, reused verbatim from companion-win/events.py | Currently dead weight at runtime (Tier-2 stubs never feed them) but is the exact shape a future Mac record-all-workbooks implementation would need — kept intentionally instead of deleted |
installer/scripts/postinstall | shell script | Root-run, per-user setup: sideload manifest, stop old instance, generate+trust TLS cert, write+bootstrap LaunchAgent | Idempotent by construction (re-run = repair/upgrade) — this is the entire "why does clicking the pkg twice not break anything" story |
setup.py | py2app OPTIONS plist | Sets LSUIElement: True (no Dock icon) and NSAppleEventsUsageDescription (required text for the Automation permission prompt on a bundled, non-Terminal app) | Without the usage string, a packaged app's Apple Events to Excel can be denied silently instead of prompting — this is the difference between "user sees a permission dialog" and "companion silently never works" |
_apply_cross) is applied only when
_is_companion_ref confirms the workbook's on-disk path lives under the companion's own
REF_DIR temp directory — a user-opened original never qualifies. save_copy's live path
copies sheets into a disposable throwaway workbook rather than ever calling Mac's SaveAs-semantics save
workbook as on the real open workbook.
http://127.0.0.1 — silently,
with zero packets sent (mac-verified with a signed WKWebView probe, 2026-07-15). Chromium/WebView2 on Windows
exempts loopback from this rule; WebKit does not. This is why tls.py exists at all, and why the server
falls back to plain http only when no cert pair is present (dev-only path — the pane cannot connect over it).
list_workbooks(), save_copy(), spotlight()'s entry check, and
_find_wb() all explicitly call _excel_or_none() (a promptless
pgrep -x "Microsoft Excel") and return an empty/None result before touching AppleScript. However
open_ref(), open_refs(), _maximize(), and _active_wb_name()
issue their own tell application "Microsoft Excel" block with no such check — on
real macOS, addressing an app via tell application launches it if it isn't running. In practice this
is inert only because these paths are reachable exclusively from the Office.js task pane, which cannot exist
unless Excel is already running the add-in — there is no code-level enforcement of the "never launches Excel"
invariant on this subset of calls, contradicting the module's own docstring claim ("every script ... only if
it's already running"). This mirrors companion-win's "attach only" rule (R2 in the design notes) in intent, and is
also why Excel detection is cheap enough to poll every 5s from the tray — but the mac backend's implementation of
that intent is incomplete.
repeat with w in workbooks and
in (every workbook) both fail with AppleScript error -50 on Excel for Mac; every loop in
excel_applescript.py uses repeat with i from 1 to (count of workbooks) /
workbook i instead.save workbook as has SaveAs semantics (retargets the open
workbook) and is additionally blocked by Excel's App Sandbox when the target path is outside
~/Library/Containers/com.microsoft.Excel/Data — and it still retargets the workbook even when the write
fails, leaving it "saved=true" against a file that was never written. save_copy's live path routes
around both failure modes.set selection to range X is asynchronous/deferred on Excel-for-Mac — it does not
commit inside the apple-event, so any script line immediately after it can race the pending selection. The
Provenance Spotlight uses the select verb instead, which commits synchronously (mac-verified across 7
range shapes: whole columns/rows, bounded, single cell, multi-area).formula 1 is read-only after creation (AppleScript error
-10006) — the cross-hair cue is always delete+recreate on move, never mutated in place.open "path" intermittently no-ops (exits 0, opens nothing); the dictionary
form open workbook workbook file name <path> is used instead, followed by a re-verify
(_find_wb) so a silent failure surfaces as a real HTTP error rather than a phantom success.read only true flag on open workbook is accepted but ignored —
opened refs are always read-write on Mac (Windows achieves true ReadOnly). Accepted as-is because the ref is always a
throwaway temp copy under REF_DIR, never the user's file.open_ref saves/restores ask to
update links and display alerts around the open (wrapped in try/end try
so the restore still runs if the open itself errors) — a generous 30s timeout keeps the "restore never runs because
the process was killed" case rare.excel-busy timeout the pane sees is always 8s, never the backend call's own
timeout. Worker.submit()'s wait is a single fixed value (Worker(backend)'s
default, 8s) set once in main.py — it has no idea that open_ref passes its own
osascript subprocess a 30s timeout. So a slow /open call always 503s the caller at 8s while
the worker thread stays occupied for up to 30s in the background; anything queued behind it (the worker is a single
thread) can itself 503 at +8s without the backend ever having started it. Not a correctness bug (the add-in already
tolerates and retries 503s) but worth knowing before "raise the osascript timeout" is proposed as a fix for a slow-open
report — it wouldn't change what the pane observes.ExecuteMso equivalent (that's Windows-COM-only), no AppleScript dictionary entry, no plist
key (all audited). The cross-hair conditional-format sweep in _activate_script is a pure emulation, not
a toggle of the real feature. _excel_short_version/_version_ge exist as the version-gate
primitive a future native Windows-side route (or a later Mac route) would need, but are informational-only today.main.py's
_pid_alive() self-heals a stale lock (process no longer exists), unit-tested by
test_single_instance_lockfile_self_heals.tray_mac.run() is called and blocks — the same
main-thread-ownership split as companion-win's pystray tray, just for the opposite reason (pystray needs the main
thread on Windows; rumps needs it on macOS)./record/* stubs return the exact shapes companion-win's real implementation returns, so the add-in
degrades gracefully (zero cross-workbook change events, no error). When it is built, the module docstring in
excel_applescript.py flags that AppleScript has no COM-style push event sink
(DispatchWithEvents/OnSheetChange equivalents don't exist) — Mac will need a
polling-diff approach, a genuinely different mechanism from the Windows implementation, not a straight port.codesign, notarytool
submit --wait, and stapler staple — none of which has been done yet (owner-side, tracked in
README "Notarization / signing")..icns asset has been wired into
setup.py's iconfile yet.
Two tiers, both under tests/. test_server.py is the everyday suite: a
FakeExcel stand-in matches ExcelAppleScript's public method surface so
server.py/Companion/Worker/auth.py/events.py and
the pure helpers (_posixify, _activate_script, _cross_formula,
_parse_workbook_lines, _version_ge, etc.) are exercised with no real Excel/osascript —
safe on any CI machine including non-Mac. e2e_live_excel.py and
e2e_focus_lifecycle.py are LIVE gates: they boot the real
ExcelAppleScript backend and drive the actual HTTP surface against a running Excel 365 for Mac session
(macOS-only, require Excel open; skip cleanly otherwise). e2e_focus_lifecycle.py in particular is the
regression gate for the 0.3.2 field bugs — it asserts name of active workbook through the full
open → spotlight → clear → streak → collision lifecycle against a real link-bearing reference file. All three are
plain-assert scripts (no pytest dependency required, though pytest can collect them) — see file docstrings for
exact run commands.