apps/companion-win — Python tray helper that gives the Excel add-in COM access to Excel it otherwise can't have.
The Companion is a small, optional Windows-native helper process that runs alongside a locally-installed
Excel and gives the Office.js task pane (apps/addin-excel) capabilities Office.js structurally cannot provide:
enumerating other open workbooks, pulling their bytes so cross-workbook VLOOKUP/external references
resolve, watching Application.SheetChange across every non-primary workbook while a recording is in progress,
and driving "Provenance Spotlight" — bringing a source workbook's window forward during replay so a viewer can see where
a referenced value actually came from. The add-in's own worksheets.onChanged subscription already records the
primary workbook; the Companion is told which workbook to exclude so events are never double-counted.
It is written in Python (apps/companion-win/src/companion/) and talks to Excel exclusively through
COM automation (pywin32's win32com.client), never by driving the UI. It exposes a tiny
loopback-only HTTP JSON API on 127.0.0.1, port-scanned by the add-in in the range 38743–38747. It runs
headless with a system-tray icon as its only visible surface (pystray, with a hand-rolled
ctypes/Shell_NotifyIcon fallback if pystray/Pillow fail to import). There is no
window, no console (built with --windows-console-mode=disable), and it is launched either from a Start-Menu
shortcut, an HKCU Run autostart key, or via explorer.exe immediately after install.
Per CLAUDE.md's "never launch Excel" constraint, the Companion only attaches to an Excel that is
already running (win32com.client.GetActiveObject("Excel.Application")) — it never starts a new Excel process, so
if Excel isn't open, every workbook-dependent endpoint just reports excelDetected: false.
Build/deploy: the shipping artifact is a native-compiled binary produced by
build-nuitka.ps1 (Nuitka, --standalone, Python → C → machine code, no shippable .pyc), named
ExcelRewindCompanion.exe. build.ps1 (PyInstaller onefile) is kept only as a legacy fallback build path
into a separate dist\ folder. The compiled .dist folder is staged into the unified NSIS installer
(apps/installer/ExcelRewind-Setup.nsi, Section SEC_COMPANION), which also owns upgrade/uninstall
shutdown sequencing (§4.4) and a per-user, non-admin autostart/shortcut path. There is also a standalone, source-based
per-user installer, install-companion.ps1, that copies the exe to %LOCALAPPDATA%\ExcelRewind and
optionally sets the HKCU Run key.
flowchart TB
subgraph ENTRY["Entry points"]
RUNPY["run_companion.py
(sys.path shim + main())"]
MAIN["main.py
main()"]
end
subgraph GUARD["Single-instance guard (main.py)"]
MUTEX["_acquire_single_instance()
Local named mutex"]
PROBE["_another_companion_running()
GET /status on 38743-38747"]
end
subgraph COMLAYER["COM layer (excel_com.py)"]
WORKER["ComWorker
STA thread + Queue, 2s timeout"]
COMEXCEL["ComExcel
real backend: GetActiveObject only"]
FAKEEXCEL["FakeExcel
in-memory test backend"]
EVBUF["EventBuffer
(events.py) coalesce+cap"]
SPANBUF["SpanBuffer
(events.py) spotlight spans"]
end
subgraph HTTPLAYER["HTTP server (server.py)"]
BIND["bind_server()
first free port 38743-38747"]
HANDLER["Handler(BaseHTTPRequestHandler)
routes /status /pair /workspace /file
/open /open-many /record/* /spotlight*"]
COMPANION_STATE["Companion
shared state: paired, recording,
excel_cached, spotlight_active"]
WATCHDOG["_watchdog thread
check_idle() every 2s"]
end
subgraph AUTHMOD["Auth (auth.py)"]
AUTHCLS["Auth
token check() / pair()"]
TOKENFILE["%APPDATA%\\ExcelRewind\\token.txt"]
DIALOG["confirm_pairing_dialog()
native MessageBoxW Yes/No"]
end
subgraph TRAYMOD["Tray (tray.py)"]
TRAYRUN["run() — pystray icon
Status / Quit menu"]
TRAYFALLBACK["_run_ctypes()
Shell_NotifyIcon fallback"]
end
subgraph WINFOCUSMOD["winfocus.py"]
FOCUS["bring_to_foreground()
find_window_by_title()"]
end
RUNPY --> MAIN
MAIN --> PROBE --> MUTEX
MAIN --> WORKER
WORKER --> COMEXCEL
COMEXCEL --> EVBUF
COMEXCEL --> SPANBUF
MAIN --> AUTHCLS
AUTHCLS --> TOKENFILE
AUTHCLS --> DIALOG
COMEXCEL -.spotlight foreground.-> FOCUS
MAIN --> COMPANION_STATE
COMPANION_STATE --> WORKER
COMPANION_STATE --> AUTHCLS
MAIN --> BIND --> HANDLER
HANDLER --> COMPANION_STATE
MAIN --> WATCHDOG --> COMPANION_STATE
MAIN --> TRAYRUN
TRAYRUN -.import fails.-> TRAYFALLBACK
TRAYRUN --> COMPANION_STATE
FAKEEXCEL -.tests only.-> WORKER
Solid arrows = direct calls/composition inside the process. Dotted arrows = best-effort/optional paths (test-only backend, OS focus-steal workaround, tray import fallback).
flowchart LR ADDIN["Excel add-in
(apps/addin-excel/src/companion.ts)"] -->|"HTTP/1.1 JSON, loopback,
Bearer token"| HANDLER["server.py Handler
do_GET / do_POST"] HANDLER -->|"lambda b: b.method(...)
queued on ComWorker"| WORKER["ComWorker._q (Queue)"] WORKER -->|"fn(backend) on STA thread"| COMEXCEL["ComExcel"] COMEXCEL -->|"GetActiveObject('Excel.Application')
Workbooks / SaveCopyAs / Range.Goto"| EXCEL["Running Excel.exe
(COM Application object)"] EXCEL -->|"SheetChange / WindowActivate /
SheetSelectionChange events (sync callback)"| SINK["_Events sink
(DispatchWithEvents)"] SINK -->|"cell {workbook,sheet,addr,value,formula}"| EVBUF["EventBuffer.add()"] SINK -->|"focus/select spans"| SPANBUF["SpanBuffer.focus()/select()"] EVBUF -->|"poll(since) -> {events[], dropped}"| HANDLER SPANBUF -->|"poll() -> spans[]"| HANDLER HANDLER -->|"JSON response"| ADDIN COMEXCEL -->|"bytes via SaveCopyAs(tmp) -> read -> delete tmp"| HANDLER HANDLER -->|"application/octet-stream"| ADDIN
flowchart LR ADDIN["add-in: attached reference
workbook bytes (base64)"] -->|"POST /open or /open-many
{name, data_b64}"| HANDLER["server.py"] HANDLER -->|"base64.b64decode + 100MB cap"| BYTES["raw bytes"] BYTES -->|"_open_ref(name, data)"| COMEXCEL["ComExcel"] COMEXCEL -->|"write file"| REFDIR["%TEMP%\\ExcelRewind\\refs\\<safe-name>"] COMEXCEL -->|"unblock_file(): remove :Zone.Identifier ADS"| REFDIR COMEXCEL -->|"Workbooks.Open(path, ReadOnly=True)"| EXCEL["Excel.exe"] COMEXCEL -->|"maximize + refocus primary"| EXCEL COMEXCEL -->|"{opened, alreadyOpen} or per-file error"| HANDLER --> ADDIN
| Path | Written by | Contents / format | Lifetime |
|---|---|---|---|
%APPDATA%\ExcelRewind\token.txt | auth._load_or_create_token() | 32 hex-char pairing token (ASCII) | Persists across restarts; one token per install, re-used by the add-in via localStorage key rewind.companion-token |
%APPDATA%\ExcelRewind\companion-port.txt | main.main() | ASCII decimal port number | Rewritten every launch; optional discovery aid (the add-in primarily port-scans) |
%TEMP%\ExcelRewind\refs\<name> | excel_com._open_ref | Raw bytes of an attached reference workbook, under its real filename | Left on disk (Excel has it open read-only); overwritten idempotently on next open of the same name |
OS temp file (tempfile.mkstemp) | ComExcel._save_copy | SaveCopyAs snapshot of an open workbook, up to 100 MB | Deleted in a finally immediately after being read into memory and streamed to the add-in |
The module never talks to Supabase, Stripe, or any cloud endpoint directly — its only network
surface is the loopback HTTP server described in §5. All .rewind assembly, upload, and cloud sync happen in
apps/addin-excel, not here; this module only ever produces raw cell-change JSON and workbook bytes over
loopback.
sequenceDiagram
participant OS as "Windows (shortcut/autostart/explorer.exe)"
participant Main as "main.py main()"
participant Probe as "_another_companion_running()"
participant Mutex as "CreateMutexW"
participant Worker as "ComWorker/ComExcel"
participant HTTP as "bind_server()"
participant Tray as "tray.run()"
OS->>Main: launch ExcelRewindCompanion.exe
Main->>Probe: GET /status on 38743..38747 (300ms each)
alt a live companion (any version) answers with product marker
Probe-->>Main: True
Main->>OS: MessageBoxW "already running"
Main->>Main: sys.exit(0)
else nothing answers
Main->>Mutex: CreateMutexW("Local\ExcelRewindCompanion.SingleInstance")
alt ERROR_ALREADY_EXISTS
Mutex-->>Main: already held
Main->>OS: MessageBoxW "already running"
Main->>Main: sys.exit(0)
else acquired
Main->>Worker: ComExcel() + ComWorker(backend) — spawns STA thread
Main->>Main: Auth() loads/creates token.txt
Main->>HTTP: bind_server() — first free port in range
Main->>Main: spawn _watchdog thread (check_idle every 2s)
Main->>Main: write companion-port.txt
Main->>Main: register atexit + SIGINT/SIGTERM/SIGBREAK -> _shutdown
Main->>Tray: run_tray(companion, port, _shutdown) — blocks main thread
Note over Tray: pystray import/init failure -> _run_ctypes fallback,
total tray failure -> httpd.serve_forever() headless
end
end
sequenceDiagram
participant Addin as "add-in companion.ts"
participant Handler as "server.py Handler"
participant Auth as "auth.py Auth"
participant User as "Windows user"
Addin->>Handler: scanPorts() — GET /status on each port/scheme
Handler-->>Addin: 200 {product:"excelrewind-companion", version, excelDetected}
Addin->>Handler: POST /pair (Origin: https://addin.excelrewind.com)
Handler->>Handler: _origin_host_ok() — Origin allow-listed AND Host = loopback:port
alt origin/host check fails
Handler-->>Addin: 403 forbidden-origin
else ok
Handler->>Auth: pair(origin)
alt a dialog is already open
Auth-->>Handler: raise PairingBusy
Handler-->>Addin: 409 pairing-in-progress
else
Auth->>User: MessageBoxW "Allow <origin> to connect to the Companion?"
(force-foregrounded via AttachThreadInput)
alt user clicks Yes
User-->>Auth: IDYES
Auth-->>Handler: token (32 hex chars)
Handler->>Handler: companion.mark_seen() — paired=True
Handler-->>Addin: 200 {token}
Addin->>Addin: localStorage.setItem('rewind.companion-token', token)
else user clicks No / dialog times out
User-->>Auth: IDNO / non-6
Auth-->>Handler: None
Handler-->>Addin: 403 pairing-declined
end
end
end
sequenceDiagram
participant Addin as "add-in (records primary itself)"
participant Handler as "server.py"
participant Companion as "Companion (shared state)"
participant Worker as "ComWorker (STA queue)"
participant Com as "ComExcel"
participant Excel as "Excel.exe (COM events)"
Addin->>Handler: POST /record/start {exclude:"Primary.xlsx", captureChanges:true}
Handler->>Companion: start_record(exclude, captureChanges)
Companion->>Worker: submit(b.start_record)
Worker->>Com: start_record(exclude, capture_changes)
Com->>Excel: DispatchWithEvents(app, _Events) — hooks OnSheetChange/
OnWindowActivate/OnSheetSelectionChange
Com-->>Handler: recording started (Application ref retained while recording)
Handler-->>Addin: 200 {recording:true, exclude}
loop every ~2-3s while recording
Excel-->>Com: OnSheetChange(sh, target) [sync COM callback]
Com->>Com: EventBuffer.add() per changed cell (coalesced 500ms/cell)
Addin->>Handler: GET /record/events?since=<seq>
Handler->>Companion: _touch_poll() (feeds R12 idle watchdog)
Handler->>Worker: submit(b.poll_events(since))
Worker->>Com: PumpWaitingMessages() then buffer.poll(since)
Com-->>Handler: {events:[...], dropped, spotlights:[...]}
Handler-->>Addin: 200 JSON
end
alt add-in stops polling for >30s (R12)
Companion->>Companion: _watchdog.check_idle() detects idle > RECORD_IDLE_TIMEOUT
Companion->>Worker: submit(b.stop_record) — auto-stop
else add-in explicitly stops
Addin->>Handler: POST /record/stop
Handler->>Companion: stop_record()
Companion->>Worker: submit(b.stop_record)
Worker->>Com: stop_record() — pump, close final span, drop sink + app ref
Com-->>Handler: completed spotlight spans[]
Handler-->>Addin: 200 {recording:false, spotlights:[...]}
end
sequenceDiagram participant Installer as "ExcelRewind-Setup.nsi StopCompanion" participant PS as "hidden PowerShell" participant Handler as "server.py /shutdown" participant Main as "main._shutdown()" participant Worker as "ComWorker/ComExcel" Installer->>PS: Invoke-RestMethod POST http://127.0.0.1:<port>/shutdown
for port in 38743..38747 PS->>Handler: POST /shutdown (no bearer token, no Origin header) Handler->>Handler: _origin_host_ok() — Origin absent is OK, Host must be loopback:port alt origin present but not allow-listed Handler-->>PS: 403 forbidden-origin else ok (local CLI has no Origin) Handler-->>PS: 200 {stopping:true} (responds BEFORE tearing down) Handler->>Main: spawn thread -> sleep 0.3s -> shutdown_hook() Main->>Main: _shutdown(): stop_event.set(), httpd.shutdown() Main->>Worker: worker.close() — submit shutdown(), join STA thread Worker->>Worker: ComExcel.shutdown(): drop sink, release Application ref,
CoUninitialize() ON the STA thread end Installer->>Installer: Sleep 1500ms Installer->>Installer: taskkill /IM ExcelRewindCompanion.exe (graceful WM_CLOSE) Installer->>Installer: Sleep 1500ms Installer->>Installer: taskkill /F /IM ExcelRewindCompanion.exe (final fallback) Note over Installer,Worker: Per main.py, the COM ref is only held DURING active
recording, /shutdown makes even that case clean, and a
force-kill while idle already holds no ref (ghost-Excel fix).
sequenceDiagram
participant Viewer as "add-in replay (or web viewer, via add-in bridge)"
participant Handler as "server.py"
participant Com as "ComExcel"
participant Excel as "Excel windows"
participant Win as "winfocus.py"
Viewer->>Handler: POST /spotlight {name, sheet, range}
Handler->>Com: spotlight(name, sheet, rng) via ComWorker
Com->>Com: capture current ActiveWorkbook.Name as _spotlight_prev (once)
Com->>Excel: wb.Activate(), sheet.Activate(), app.Goto(range, True)
Com->>Win: bring_to_foreground(hwnd) — AttachThreadInput dance
(defeats the foreground-lock timeout)
alt named workbook is not open
Com-->>Handler: raise FileNotFoundError
Handler-->>Viewer: 404 not-open (replay continues without the freeze)
else opened
Com-->>Handler: restore-target name
Handler-->>Viewer: 200 {spotlighted:true, restore}
end
Viewer->>Handler: POST /spotlight/clear (when the freeze dwell ends)
Handler->>Com: clear_spotlight()
Com->>Excel: re-Activate the pre-spotlight workbook
Handler-->>Viewer: 200 {cleared:true, restored}
Note over Handler: If the viewer crashes/closes mid-freeze and never clears,
the 2s watchdog auto-clears after SPOTLIGHT_IDLE_TIMEOUT=15s
so focus can never be stranded on the source workbook.
flowchart LR ADDIN["addin-excel
(apps/addin-excel/src/companion.ts)"] <-->|"loopback HTTP/1.1 JSON,
Bearer token, CORS-locked"| COMPANION["companion-win"] COMPANION -->|"COM: GetActiveObject,
Workbooks/Range/Application events"| EXCEL["Microsoft Excel
(local process, already running)"] COMPANION -->|"user32.dll: MessageBoxW,
SetForegroundWindow, Shell_NotifyIcon"| WINOS["Windows OS APIs"] INSTALLER["installer-deploy
(ExcelRewind-Setup.nsi)"] -->|"POST /shutdown (no auth),
then taskkill ladder"| COMPANION INSTALLER -->|"stages Nuitka .dist folder
into the installer payload"| COMPANION
| Peer | Direction | Protocol | Purpose |
|---|---|---|---|
addin-excel | in/out | HTTP/1.1 over 127.0.0.1:38743-38747, JSON bodies, CORS restricted to https://addin.excelrewind.com and https://localhost:3000, Bearer-token auth on every endpoint except /status and /pair | Enumerate/attach other open workbooks, stream cross-workbook cell-change events, save/attach workbook bytes, drive Provenance Spotlight during replay |
| Microsoft Excel (local process) | out (attach only) | COM automation via pywin32 (win32com.client.GetActiveObject("Excel.Application"), DispatchWithEvents for SheetChange/WindowActivate/SheetSelectionChange) | Read workbook list/bytes, watch non-primary cell edits, open reference workbooks read-only, drive window focus/selection for Spotlight. Never launches Excel. |
Windows OS (user32/shell32/kernel32 via ctypes) | out | Win32 API calls (no network) | Native pairing/status MessageBoxW dialogs, focus-steal-guard workaround (AttachThreadInput + SetForegroundWindow), tray icon (pystray → Shell_NotifyIcon), single-instance named mutex |
installer-deploy (apps/installer/ExcelRewind-Setup.nsi) | in | HTTP POST /shutdown via hidden PowerShell Invoke-RestMethod (no auth token — gated by Origin/Host check instead), then taskkill | Clean COM teardown before overwriting the binary on upgrade, or on uninstall |
Filesystem (%APPDATA%, %TEMP%) | in/out | Local file I/O, no network | Persist pairing token, discovery port marker, staged reference-workbook copies (§3.3) |
This module has no direct edge to Supabase, Stripe, or any Anthropic/Claude API — it is a pure local bridge between the add-in's web-view sandbox and the user's own running Excel process.
| File | Function / export | What it does | Why it matters |
|---|---|---|---|
src/companion/main.py | _acquire_single_instance(), _another_companion_running() | Named-mutex + port-probe double guard against a second companion instance | The historical field bug: a stacked launch each held a COM ref and left a ghost EXCEL.EXE that blocked the add-in cache from clearing. The port-probe belt exists because a pre-mutex (<v0.2.0) build has no mutex to see. |
src/companion/main.py | main() / _shutdown() | Wires backend→worker→auth→server→tray; registers atexit + SIGINT/SIGTERM/SIGBREAK handlers all funneling into one idempotent _shutdown | Every exit path (console close, signal, tray Quit, /shutdown POST) releases the COM ref exactly once; a bare taskkill /F is documented as safe only while idle (no ref held) — during active recording it would still leak. |
src/companion/excel_com.py | ComWorker (class) | Single dedicated STA thread; HTTP handler threads submit() a callable and block up to 2s for a result box, else ExcelBusy | Excel's COM apartment is single-threaded; this is the only thing standing between concurrent add-in requests and RPC_E_CALL_REJECTED. Every COM call in the process funnels through here. |
src/companion/excel_com.py | ComExcel._attach() / _idle_op() / _release_app() | GetActiveObject-only attach with a liveness probe; releases the Application COM reference after every call unless recording is active | Root-cause fix for the ghost-Excel bug: an idle companion holding zero COM refs means force-killing it can never orphan Excel. Documented explicitly in the source (lines 376-399). |
src/companion/excel_com.py | _install_sink(), OnSheetChange/OnWindowActivate/OnSheetSelectionChange | DispatchWithEvents sink hooked onto the Application; conditionally hooks the expensive OnSheetChange (reads every changed cell's formula+value) only when capture_changes (Record-all-workbooks) is requested — spotlight-only recording skips it | The COM sink executes synchronously inside Excel's UI thread; an exception here would crash into Excel, hence the blanket except Exception: pass in every handler. pump() (COM message pumping every ~50ms on the idle STA loop) exists specifically because without it the synchronous dispatch stalled Excel's UI — "painfully slow" field feedback. |
src/companion/excel_com.py | _safe_ref_name(), unblock_file() | Strips path components to a bare filename (defeats ..\/absolute-path traversal); removes the NTFS Zone.Identifier ADS (Mark-of-the-Web) so Excel doesn't show Protected View | Security boundary on untrusted name input from the add-in; the MOTW strip only ever touches a file this process itself just wrote under the user's own temp dir, never a downloaded file the user hasn't already trusted. |
src/companion/excel_com.py | ComExcel._retry() | Retries a COM call up to 3× (200ms backoff) specifically on RPC_E_CALL_REJECTED (-2147418111) | Bounds the "Excel is busy" surface — a modal dialog or in-progress paste in Excel rejects re-entrant COM calls; this absorbs transient rejections before the caller sees 503. |
src/companion/excel_com.py | ComExcel.spotlight() / clear_spotlight() / _activate() | Brings a source workbook's window forward, activates a sheet/range, remembers the pre-spotlight active workbook, restores it on clear | Provenance Spotlight Tier-2 UX; _spotlight_prev is a workbook name (string), not a COM ref, specifically so it survives the idle-release cycle safely. |
src/companion/events.py | EventBuffer | Per-cell 500ms coalescing window + a 5000-event drop-oldest deque with a running dropped counter; monotonic seq for incremental polling | Bounds memory under an edit flood (e.g. a huge paste) and lets the add-in poll with ?since=<seq> without re-fetching everything. Pure logic, unit-testable without Excel. |
src/companion/events.py | SpanBuffer | Tracks focus/selection "spans" on non-primary workbooks (open span → close on next focus change or stop); drops sub-600ms flicker spans, merges re-entries to the same file/sheet/range within 2s | Raw material for exact recorded Provenance Spotlight windows, including provenance of hand-typed (non-formula) values. A wandering author clicking through files doesn't spawn a spotlight storm. |
src/companion/server.py | Companion (class) | Shared mutable state behind one lock: recording flag, last-poll timestamp, paired/connected liveness, cached excelDetected, spotlight-active timestamp | connected() (12s window) vs paired (latched forever) is what makes the tray's Status box accurate; excel_cached() is what lets /ping answer without ever touching COM. |
src/companion/server.py | Companion.check_idle() / _watchdog() | 2s-tick watchdog: auto-stops recording after 30s with no /record/events poll (R12); auto-clears a stale spotlight after 15s | The two "can't strand the user" safety nets — a crashed/closed add-in pane can't leave Excel silently recording forever, nor leave focus stuck on a source workbook. |
src/companion/server.py | Handler._origin_host_ok() | CSRF guard for pre-auth POSTs (/pair, /shutdown): allow-lists Origin if present, always requires Host to equal the bound loopback port | These two endpoints intentionally require no bearer token (chicken-and-egg for pairing; no token available to the local uninstaller) so this is the entire defense against a malicious webpage hitting the loopback port. |
src/companion/server.py | bind_server() | Binds the first free port in PORTS = [38743..38747] | Defines the exact range the add-in port-scans (apps/addin-excel/src/companion.ts PORTS constant) — the two must stay in sync. |
src/companion/auth.py | _load_or_create_token() | 32-hex-char token, generated once with secrets.token_hex(16), persisted to token.txt | The entire bearer-auth scheme's root secret; loss/rotation means the add-in must re-pair. |
src/companion/auth.py | Auth.pair() / _pair_lock | Non-blocking lock around showing the native consent dialog; a concurrent /pair raises PairingBusy instead of stacking a second MessageBoxW | Fixes a real UX bug: ThreadingHTTPServer handles each POST on its own thread, so retried clicks used to stack blocking dialogs. |
src/companion/auth.py | _force_foreground(), confirm_pairing_dialog() | Finds the modal by window class #32770 + caption and forces it to the OS foreground via the AttachThreadInput trick | Works around the Windows XP+ focus-steal guard that otherwise leaves a background process's MessageBoxW merely flashing in the taskbar. |
src/companion/tray.py | run() / _run_ctypes() | Primary path: pystray icon with Status/Quit menu, 5s tooltip refresh. Fallback: hand-rolled Shell_NotifyIcon window if pystray/Pillow fail to import at runtime | The exe is windowless/consoleless by design — the tray icon is the only "it's running" signal for the user, so both paths matter for field builds where a dependency didn't bundle correctly. |
src/companion/winfocus.py | bring_to_foreground(), find_window_by_title() | Foreground-steal workaround (drop the lock timeout, AttachThreadInput, SetForegroundWindow); called only from Provenance Spotlight's window activation (excel_com.py's _activate()) | Despite the module's own docstring claiming it's shared with the pairing dialog, auth.py never imports winfocus — confirm_pairing_dialog() has its own separate, near-identical _force_foreground() implementation (see the auth.py row above). The same Windows quirk is solved twice, not once — doc corrected 2026-07-17 against the actual imports. |
build-nuitka.ps1 | (script) | Compiles run_companion.py with Nuitka --standalone into dist-nuitka\...\ExcelRewindCompanion.exe, forcing win32com/pythoncom/pystray/PIL as explicit includes (COM sub-packages are lazily imported and Nuitka's static analysis misses them) | This is the shipping build since 2026-07-16 — no .py/.pyc ships, only compiled machine code (plus stdlib/dep DLLs, which are not proprietary code). build.ps1 (PyInstaller) is legacy fallback only. |
win32com.client.GetActiveObject("Excel.Application")
only; if that fails, excelDetected is simply false. There is no code path in this module that starts a
new Excel process — this is a direct instance of the repo-wide "never modify/launch the user's spreadsheet environment
outside their control" constraint.save_copy() uses SaveCopyAs to a throwaway temp
file, read then immediately deleted; reference workbooks are opened with ReadOnly=True.127.0.0.1 exclusively (never
0.0.0.0); CORS is hard-coded to the two known add-in origins; every endpoint except /status, /pair,
and /shutdown requires a valid Bearer token compared with secrets.compare_digest (constant-time). This
module sends only cell-change events + workbook bytes over that channel — it never phones any cloud endpoint itself,
consistent with the product-wide "add-ins send only session events, never uploaded to a general API" posture (the
add-in decides what enters a .rewind file, not this process).Application COM reference is retained
between calls only while actively recording; every idle operation releases it in a finally
(_idle_op). This is why a force-kill of an idle companion is documented as safe, but a force-kill
during recording is not (the sink still holds the ref) — /shutdown exists specifically to make even
that case clean before the installer's taskkill fallback ladder runs.ComWorker); no other thread in the process is allowed to touch a COM object directly. A 2-second queue timeout
maps unavailability to HTTP 503 excel-busy rather than blocking the HTTP thread pool.GetActiveObject cannot cross a Windows integrity-level
boundary: an elevated (admin) companion process cannot see a normal-integrity Excel. The NSIS installer runs elevated
(needed for net share elsewhere in the install) but deliberately launches the companion via
Exec 'explorer.exe "...\ExcelRewindCompanion.exe"' to re-parent it to the user's normal-integrity shell —
launching it directly from the elevated installer process would silently produce "Excel not detected" forever.OnSheetChange/OnWindowActivate/
OnSheetSelectionChange fire synchronously and block Excel's UI until they return — every handler body is wrapped
in a blanket except Exception: pass because an unhandled exception here would propagate into Excel itself. The
STA worker's idle loop pumps COM messages every ~50ms specifically so these synchronous callbacks return promptly;
without it, editing while recording was reported as "painfully slow."captureChanges=false skips hooking
OnSheetChange entirely (only focus/selection spans are tracked) because reading every changed cell's
Formula+Value2 is expensive on a large paste; this is a deliberate, request-controlled cost knob, not
an oversight./record/events poll (R12 — a crashed pane can't leave Excel silently recording forever); a spotlight
auto-clears after 15s idle so window focus can never be permanently stuck on a source workbook if the viewer never
calls /spotlight/clear._safe_ref_name() strips to a bare basename before
any file is written under %TEMP%\ExcelRewind\refs\, defeating a crafted name like ..\..\evil.xlsx
from the add-in's JSON body.unblock_file() only ever
strips the Zone.Identifier ADS from a path inside the ref-staging temp dir that _open_ref just created
— it is not a general "bypass Protected View" utility.Local\ExcelRewindCompanion.SingleInstance)
blocks a second mutex-aware launch; a port-probe (GET /status for the product marker across all five ports)
additionally blocks pre-mutex (<v0.2.0) builds that a plain mutex check can't see. Both exist because the field bug
this fixes (a stacked swarm of companions each holding a COM ref) was severe enough to want two independent guards.README.md, the exe is currently unsigned, so SmartScreen warns on first
run ("Windows protected your PC" → More info → Run anyway). No code-signing certificate is applied yet.companion-build-nuitka.md) and build-nuitka.ps1's own header comment, shipped artifact names must
never mention the compiler; the exe stays ExcelRewindCompanion.exe regardless of build tool. A visible
.pyd/python310.dll in the standalone install folder is expected (stdlib/dependency C extensions) — the
protection goal is that ExcelRewind's own code compiles to machine code with zero shippable .py/.pyc,
not that the folder contains no evidence Python was used at all.excel_com.py's own docstring states
ComExcel must not be exercised on a dev machine that's live in Excel; all server/auth/event-buffer logic is
tested exclusively against the in-memory FakeExcel backend (tests/test_server.py, run with
.\.venv\Scripts\python.exe .\tests\test_server.py), which mirrors the idle-release/attach-count semantics
without any real COM.