Repository navigation
feat: opt-in, local shadow mode with a notice, status indicators, and draft offers - #205
Conversation
ShadowModeService runs openadapt-capture's shadow observer inside the engine. It turns on only with acknowledged=true and the hash of the current notice, resumes after a restart only while that acknowledgement stands, and "purge and disable" deletes the buffer, the key, the ignore list, and saved drafts, then records the revocation. The recorder gets the service's live tap when shadow mode is on, so Desktop recordings feed it. Review draft saves an owner-only draft file; nothing runs. The shadow commands are on the Tauri bridge only, not the local socket, and shadow mode is not part of EngineConfig, so no config, environment, or policy can turn it on. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Settings gets a Shadow mode card: the notice with Turn on shadow mode and Not now, the status (when it was turned on, the 24-hour buffer, where the key is), the work-site list, and a one-click "Delete everything and turn off". The header shows "Shadow mode on" while it is on, and offers appear as "You did this N times today." with Review draft and Ignore. The tray menu gains a "Shadow mode: on/off" line, a matching tooltip, a "Shadow" title on macOS, and a "Shadow mode settings…" item. The window sets them through the new set_shadow_tray_status command. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Drop the implication that no one can see the shadow record. OpenAdapt, an employer, and other apps can't turn it on or read it through OpenAdapt, but an OS administrator or software running as the user could inspect it while it's on. The UI test uses the new notice version, 2026-10-17.2. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
The engine and the CLI logged to stderr with loguru's default diagnose mode, which prints the value of every variable on each traceback line. The IPC handler logs every failed command with logger.exception, so a failure while handling a recording or a shadow-mode event could copy a typed value, a label, or a URL into the log. Both sinks now set diagnose=False. The test puts a value on the failing line and checks that it doesn't reach the log; it fails with diagnose on. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…port a kept key - When shadow mode was left on but couldn't resume (the notice changed, or this Capture has no shadow mode), resume returned without touching anything, so the buffer and its key stayed on disk and in the keychain indefinitely. A notice version bump does exactly this. Resume now deletes everything in that case. - A recording's live tap was bound to the observer that existed when the recording started. After a work-site change replaced the observer, the rest of the recording went to the closed one, under the old allowlist. The tap now feeds whichever observer is current, or nothing. - Delete everything and turn off reported success even when the keychain refused to delete the key. It now returns key_deleted, and the key is retried on the next destroy. - Draft and state files were written and then chmod'ed, so they existed briefly with the default umask. They're now created owner-only. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…e engine answers - Pause is in the header, the tray menu, and the Settings card, and the card advises pausing before viewing sensitive material. The header and the tray say "paused" while it is. - Settings has Turn off (erases the buffer, the key, and drafts) next to Delete everything and turn off, and says so when the keychain kept a key. - The window and the tray started out saying "off" until the first status read, and a failed read also showed "off". They now say "checking", and a failed read keeps the last known state. The status is re-read every 30 seconds and when the engine restarts. - The tray line no longer says "(local only)". The card and the header copy say the buffer is cleared at least every 24 hours and on turn-off or quit, instead of "on this computer only". Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
- Shadow files moved from <data_dir>/shadow, which a person may point at a synced folder, to the platform app-data folder: Application Support on macOS, LocalAppData on Windows. Shadow mode refuses to start if that folder resolves inside a known sync root, and files from the old location are deleted. - Quitting erased nothing: the shell wrote "shutdown" and killed the engine at once, and the engine had no shutdown command. The engine now handles shutdown ahead of queued commands by erasing the buffer and its key, then answers, and the shell waits up to 2 seconds for that answer. The engine also erases them on a normal exit and through atexit. - A buffer left by a crash is erased at the next start, before a new one begins. The engine starts shadow mode at launch instead of on the window's first status request. - New commands: shadow_pause, shadow_unpause (a pause lasts across a restart), and shadow_turn_off, which erases the buffer, the key, and drafts but keeps the ignore list. - A work-site change erases the old buffer instead of keeping it. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…quit Counsel's review: replace "nothing leaves" claims with "OpenAdapt never sends shadow mode data off this computer", note that backup, sync, and endpoint security tools can still copy local files, name IT administrators on a managed computer, and advise pausing before viewing sensitive material. SHADOW_MODE.md gains sections on where the record is kept and on pausing, and lists the new commands. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…mputer Counsel's review of the shadow mode notice applies to the rest of Desktop. "Nothing leaves this computer" and "stays on this computer" aren't true when backup, sync, or endpoint security software copies local files. Each screen now says what OpenAdapt itself does with its data, checked against the engine: - Sample: OpenAdapt doesn't send the run anywhere, and the first run downloads its browser. - Welcome and README: OpenAdapt doesn't send recordings or tasks anywhere unless you choose to share, upload, or push them. - Results: these results aren't sent, except the PHI-free outcome the hosted runner reports for a Cloud job it runs. - RecordReview and Settings: uploads happen only when you choose one, and stopped-run notes carry no PHI. The task description and target stay out of logs and aren't sent unless you upload or deploy. - Needs you: the field differences and screen crop aren't sent anywhere. - Authoring: OpenAdapt doesn't send window titles to your assistant. - Teach, the judgment-case digest, the portal doc, and a docstring. Where patient data or general recordings are involved, the copy adds that backup, sync, and security software can still copy local files. tests/test_engine/test_locality_copy.py fails if any of the old absolute phrases returns to the README, docs, UI, engine, or tray source. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
End-to-end shadow-mode checks (macOS, 2026-10-17, Toronto time)Build. Method. The app was launched with
The bundle's original launcher was restored byte-for-byte afterwards. Shadow mode is left off. Local suites after merging main. pytest: 1254 passed, 43 skipped; |
Shadow mode imports openadapt_capture.shadow (live tap, consent notice, encrypted bounded buffer, on-device motif mining), first released in openadapt-capture 1.3.0. Raise the floor and relock. The lock moves alembic to 1.20.0 and SQLAlchemy to 2.1.5 because capture 1.3.0 requires alembic>=1.20.0 and sqlalchemy>=2.0.54. The shadow tests use importorskip, so add a contract test that fails instead of skipping when the resolved capture lacks shadow mode. Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
|
Review after ce951f8 (
Signed-off-by: Richard Abrich richard.abrich@mldsai.com |
Summary
This adds shadow mode to Desktop. It's off by default, and OpenAdapt never sends its data off the computer. Once someone turns it on in Settings, it notices a task repeated across their recordings and offers to make it into a draft. It never runs anything. The watcher itself is in openadapt-capture (OpenAdaptAI/openadapt-capture#156).
What changed
openadapt_desktop/shadow_mode.py):ShadowModeServiceturns on only when it getsacknowledged: trueand the hash of the current notice.~/Library/Application Support/ai.openadapt.desktop/shadow,%LOCALAPPDATA%\ai.openadapt.desktop\shadow), not underdata_dir. It refuses to start if that folder resolves inside a known sync root.shutdownand kill the engine at once, and the engine had no such command, so quitting erased nothing. The engine now handlesshutdownfirst by erasing the buffer and key, and the shell waits up to 2 seconds for its answer.diagnose=False, so a traceback can't print a variable's value.shadow_status,shadow_notice,shadow_enable,shadow_pause,shadow_unpause,shadow_turn_off,shadow_purge_and_disable,shadow_set_work_domains,shadow_decide, andshadow_scan. The window reaches them through the Tauri bridge only; the local socket doesn't accept them. Shadow mode isn't inEngineConfig.docs/SHADOW_MODE.md(new sections on where the record is kept and on pausing), the README, anddocs/COMPLIANCE.md, using counsel's wording.Before this can merge
openadapt-capture>=1.2.1from PyPI, and that release doesn't includeopenadapt_capture.shadow. Until a Capture release with it is out and the pin is bumped, the card says shadow mode isn't available, and the shadow tests are skipped.tray.rs,sidecar.rs) passrustfmt, but I couldn't runcargo checkoffline or a Tauri build. Quit-time erasure needs a check in a real build.Tests
pytest tests/test_engine/test_shadow_mode.py tests/test_engine/test_main.py tests/test_engine/test_dispatch.py: 126 passed, 8 skipped, run against the Capture branch. A test fails if "Nothing leaves this computer" returns to the shadow copy, docs, or tray.pytest tests/test_engine/test_item_approval.py tests/test_e2e/test_ipc.py: 14 passed, 7 skipped.vitest run src/ui/ShadowMode.test.tsx: 16 passed. The threesrc/App.*.test.tsxfiles and the two Settings tests: 26 passed.tsc --noEmit: clean. Ruff is clean on the changed Python files.Decisions needed