Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 5 additions & 3 deletions .github/codeql/desktop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# CodeQL scope for the JavaScript/TypeScript and Rust analyses, which exist for
# OpenAdapt Desktop (its React UI and Tauri shell). The frozen pre-1.0 monolith
# under legacy/ is not analyzed for these languages.
# CodeQL scope for the JavaScript/TypeScript analyses: OpenAdapt Desktop
# (React UI and Tauri shell) and the optional Flow Chrome extension (MV3).
# Rust analysis uses the same file; only packages/desktop has Rust sources.
# The frozen pre-1.0 monolith under legacy/ is not analyzed for these languages.
name: openadapt-desktop
paths:
- packages/desktop
- packages/flow/chrome_extension
6 changes: 5 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ jobs:
strategy:
fail-fast: false
matrix:
# javascript-typescript and rust cover packages/desktop (React UI, Tauri shell).
# javascript-typescript covers packages/desktop and packages/flow/chrome_extension;
# rust covers the Tauri shell under packages/desktop.
language: ["python", "javascript-typescript", "rust"]
steps:
- name: Checkout
Expand Down Expand Up @@ -65,6 +66,9 @@ jobs:
changed="$(git diff --name-only "$base" HEAD)"
case "$LANGUAGE" in
python) pattern='\.py$|^\.github/codeql/|^\.github/workflows/codeql\.yml$' ;;
# Desktop UI/shell plus the optional Flow Chrome extension (MV3 JS).
javascript-typescript) pattern='^packages/desktop/|^packages/flow/chrome_extension/|^\.github/codeql/|^\.github/workflows/codeql\.yml$' ;;
rust) pattern='^packages/desktop/|^\.github/codeql/|^\.github/workflows/codeql\.yml$' ;;
*) pattern='^packages/desktop/|^\.github/codeql/|^\.github/workflows/codeql\.yml$' ;;
esac
if grep -Eq "$pattern" <<<"$changed"; then run=true; else run=false; fi
Expand Down
56 changes: 56 additions & 0 deletions packages/flow/chrome_extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Optional Chrome extension evidence (everyday Chrome)

**Status:** unpack-first scaffold for [OpenAdapt#1872](https://github.com/OpenAdaptAI/OpenAdapt/issues/1872).
Not a store listing. Not required for recording.

OpenAdapt records everyday Chrome through the OS accessibility tree with no
install. Accessibility cannot see open shadow DOM detail, cross-origin
iframes, or network saves. This Manifest V3 extension is an optional evidence
source for those gaps. Without it, recording still works.

## This slice (Part of #1872)

- Content script builds a shadow-piercing target path across **open** shadow
roots (`host >>> inner`).
- Closed shadow roots are marked (`>>closed`) and stop the walk.
- Password and declared-secret field values are replaced with `<secret>`
before a message leaves the content script.
- Evidence is sent only while a recording is active; the action badge shows
`REC`.
- A local native-messaging host stub appends lines to
`extension_evidence.jsonl` in the recording directory.
- The Flow engine reads that file through
`openadapt_flow.extension_evidence` and treats a missing file as empty.

Not in this slice (follow-up PRs): `chrome.debugger` / `webRequest` network
save capture, full cross-origin iframe DOM reads, compiler use of extension
paths in identity checks, and Chrome Web Store packaging.

## Load unpacked (local only)

1. Open `chrome://extensions`, enable Developer mode, Load unpacked → this
directory.
2. Note the extension id. Edit `native_host/com.openadapt.extension.json`:
set `path` to the absolute path of `native_host/openadapt_extension_host.py`
(or a small wrapper script) and `allowed_origins` to
`chrome-extension://<id>/`.
3. Install the host manifest where Chrome expects it for your OS.
4. Set `OPENADAPT_EXTENSION_RECORDING` to the recording directory before the
host starts.

Do not use this in a sensitive profile until network capture and reconnect
acknowledgement land. Prefer Flow's Playwright recorder or CDP attach for
governed browser recording today
([BROWSER_RECORDING.md](../docs/BROWSER_RECORDING.md)).

## Tests without Chrome

```bash
cd packages/flow
pytest tests/test_extension_evidence.py tests/test_extension_shadow_evidence.py
```

The shadow tests run the content and background scripts in Node against a fake
DOM. Real Chromium coverage for native messaging and multi-frame pages needs a
headed or headless Chrome later (office lab was unreachable at first
implementation).
115 changes: 115 additions & 0 deletions packages/flow/chrome_extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// OpenAdapt optional extension evidence (service worker).
// Relays content-script evidence to the local native host only while recording.
// Shows a visible REC badge. No remote endpoints.
'use strict';

const NATIVE_HOST = 'com.openadapt.extension';
const SCHEMA = 'openadapt.extension-evidence/1';

let recording = false;
let port = null;
let queued = [];

function setBadge(active) {
const text = active ? 'REC' : '';
const color = active ? '#b00020' : '#00000000';
try {
chrome.action.setBadgeText({ text });
chrome.action.setBadgeBackgroundColor({ color });
chrome.action.setTitle({
title: active
? 'OpenAdapt extension evidence (recording)'
: 'OpenAdapt extension evidence (idle)',
});
} catch (e) {}
}

function ensurePort() {
if (port) return port;
try {
port = chrome.runtime.connectNative(NATIVE_HOST);
port.onDisconnect.addListener(() => {
port = null;
});
} catch (e) {
port = null;
}
return port;
}

function deliver(evidence) {
if (!recording) return;
const p = ensurePort();
if (!p) {
// Native host missing: keep a short in-memory queue for tests / attach later.
if (queued.length < 1000) queued.push(evidence);
return;
}
try {
p.postMessage({ type: 'EXTENSION_EVIDENCE', schema: SCHEMA, evidence });
} catch (e) {
if (queued.length < 1000) queued.push(evidence);
}
}

function broadcastRecording(active) {
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs || []) {
if (!tab.id) continue;
try {
chrome.tabs.sendMessage(tab.id, { type: 'SET_RECORDING', active });
} catch (e) {}
}
});
}

function setRecording(active) {
recording = Boolean(active);
setBadge(recording);
broadcastRecording(recording);
if (!recording) queued = [];
}

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (!msg || typeof msg !== 'object') return;
if (msg.type === 'SET_RECORDING') {
setRecording(Boolean(msg.active));
sendResponse({ ok: true, active: recording });
return true;
}
if (msg.type === 'CONTENT_READY') {
sendResponse({ active: recording });
return true;
}
if (msg.type === 'EXTENSION_EVIDENCE') {
if (recording && msg.evidence) deliver(msg.evidence);
sendResponse({ ok: true, active: recording });
return true;
}
if (msg.type === 'GET_STATE') {
sendResponse({ active: recording, queued: queued.length });
return true;
}
});

// Expose for the Node harness.
if (typeof globalThis !== 'undefined') {
globalThis.__openadaptExtensionBackground = {
setRecording,
deliver,
get recording() {
return recording;
},
get queued() {
return queued.slice();
},
_reset() {
recording = false;
queued = [];
port = null;
setBadge(false);
},
};
}

setBadge(false);
Loading
Loading