Skip to content
Open
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@

<!-- version list -->

## Unreleased

### Features

- Add attended authentication handoffs. Capture suppresses sensitive source
data, seals a bounded method marker, and retains a fresh exact frame before
native input resumes. The same retry-safe operations are available through
the authenticated local control channel.

## v1.3.0 (2026-08-28)

_This release is published under the MIT License._

### Features

- Capture an exact macOS window through a desktop-independent
ScreenCaptureKit filter. Exact-window Quartz and system-utility providers
remain as compatibility paths.
- **capture**: Seal native action geometry at capture time
([#94](https://github.com/OpenAdaptAI/openadapt-capture/pull/94),
[`08727ab`](https://github.com/OpenAdaptAI/openadapt-capture/commit/08727ab0e471d4a6de3c460099b1a8a97758cf6f))
Expand Down Expand Up @@ -37,6 +49,14 @@ _This release is published under the MIT License._

### Bug Fixes

- Refuse ambiguous macOS window selectors instead of choosing a different
matching window.
- Keep FFmpeg outside the recorder's interrupt process group so Ctrl-C can
finish the video trailer.
- Ignore unattributable macOS modifier-flag events without terminating the
input observer.
- Report failed and finalizing sessions with `ready: false`, plus a stable
failure stage and error code.
- **ci**: Allowlist the published extension key by exact value
([#112](https://github.com/OpenAdaptAI/openadapt-capture/pull/112),
[`69a123f`](https://github.com/OpenAdaptAI/openadapt-capture/commit/69a123fccfa6dcbf848e486301832cc826ab679a))
Expand Down
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ recorder as a library.

[Documentation](https://docs.openadapt.ai) ·
[Flow](https://github.com/OpenAdaptAI/openadapt-flow) ·
[Window capture](https://github.com/OpenAdaptAI/openadapt-capture/blob/main/docs/WINDOW_CAPTURE.md)
[Window capture](https://github.com/OpenAdaptAI/openadapt-capture/blob/main/docs/WINDOW_CAPTURE.md) ·
[Authentication handoffs](https://github.com/OpenAdaptAI/openadapt-capture/blob/main/docs/AUTHENTICATION_HANDOFF.md)

## With OpenAdapt

Expand Down Expand Up @@ -88,6 +89,7 @@ A finished capture looks like this:

```text
my-capture/
├── authentication-handoffs.json
├── capture-state.json
├── recording.db
├── oa_recording-*.mp4
Expand All @@ -108,6 +110,30 @@ an authenticated session capability that lives in an owner-only runtime file
On macOS it also removes extended ACL entries and verifies they are absent. The
capability never reaches command arguments, logs, or the capture directory.

## Authentication inside a recording

Authenticate before recording when the demonstration does not need the login.
When login belongs inside the workflow, start an authentication handoff. Capture
then drops screen, input, structural, window, browser-category, and microphone
content at the source boundary. It stores a sealed marker with method classes
and booleans. It has no field for a credential or account identifier.

```python
handoff = recorder.begin_authentication(
methods="password_manager",
requires_user_presence=True,
saved_account_selected=True,
)
# Select the saved account and finish any MFA prompt.
marker = recorder.end_authentication(handoff, outcome="completed")
```

The end call returns after Capture retains a new exact frame. Input stays
blocked if that frame fails. A launcher can use the authenticated
`begin_authentication_handoff()` and `end_authentication_handoff()` process
control functions for the same contract. See
[the authentication handoff contract](https://github.com/OpenAdaptAI/openadapt-capture/blob/main/docs/AUTHENTICATION_HANDOFF.md).

## FFmpeg

Recording video needs an FFmpeg executable, and `capture install-ffmpeg` gets
Expand Down Expand Up @@ -213,6 +239,16 @@ producing media that looks complete but has an evidence gap. The full contract,
including the multi-monitor rules and the coordinate-space flags converters
must respect, is in [docs/WINDOW_CAPTURE.md](https://github.com/OpenAdaptAI/openadapt-capture/blob/main/docs/WINDOW_CAPTURE.md).

On current macOS, exact-window capture keeps one ScreenCaptureKit stream bound
to a desktop-independent window filter. The window can be occluded or on
another Space. It does not need to be frontmost or reported as on screen. The
recorder accepts complete frames and proven idle frames. A failed provider is
disabled for the rest of that recording, so each frame does not repeat a slow
failure. The recorder still needs a logged-in desktop session. A minimized
window is refused because macOS can expose stale backing pixels for it. For an
off-Space window, macOS Accessibility must confirm that the exact window is not
minimized.

Linux window mode needs X11 with EWMH and XComposite. It refuses to start under
native Wayland or XWayland-only.

Expand All @@ -222,6 +258,9 @@ A raw capture can hold everything visible on screen and everything typed:
credentials, personal data, protected health information. Keep the whole
directory inside its approved local boundary and give it a retention policy.

An authentication handoff prevents the named login interval from entering the
raw capture. Content retained before or after that interval remains sensitive.

Capture doesn't upload a session. The sharing command and the profiling
transfer are the only two transfers and both are explicit. Installing the
`privacy` extra does not scrub anything by itself.
Expand Down Expand Up @@ -263,7 +302,7 @@ Flow instead: [docs/BROWSER_EXTENSION_BOUNDARY.md](https://github.com/OpenAdaptA

## Limits

- Native recording needs a visible user session plus the operating system's
- Native recording needs a logged-in user session plus the operating system's
screen-recording and input-monitoring permissions.
- Accessibility evidence appears only where the application and the local
provider expose it. An opaque remote application still needs Flow's visual
Expand Down
184 changes: 184 additions & 0 deletions docs/AUTHENTICATION_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Authentication handoffs

An authentication handoff stops sensitive source retention while a person,
password manager, passkey provider, SSO page, or MFA device completes a login.
Capture keeps the recording open. It stores a small timeline marker and resumes
normal recording only after it has retained a new exact frame.

Authenticate before recording when that produces a complete demonstration.
Use a handoff when authentication must occur inside the workflow or when a
session can expire during a long recording.

## What Capture suppresses

The protected interval covers every source owned by the native recorder:

| Source | Behavior during the handoff |
| --- | --- |
| Screen and video | Capture retains no frame. The encoded video holds its prior visual state until the fresh resume frame. |
| Mouse and keyboard | The native observer drops the event before journal reservation and before any structural lookup. |
| Accessibility | UIA, AX, and AT-SPI observations do not run for protected input. |
| Window metadata | Capture does not retain active-window titles, bounds, or state. |
| Audio | The audio process replaces microphone chunks with generated silence so the audio clock stays aligned. It does not retain the protected waveform. |
| Browser category | The marker declares browser data suppressed. The supported Playwright browser recorder remains in `openadapt-flow` and needs its own matching source boundary. |

The recorder does not add black frames or synthetic action rows. A black frame
can look like a real application state. An action row without pixels can look
replayable. The sidecar names the gap directly.

## What the marker contains

`authentication-handoffs.json` uses
`openadapt.capture.authentication-handoffs/v1`. The capture seal inventories
its exact bytes. Each interval contains:

- a random interval UUID;
- one or more method classes from `password_manager`, `passkey`, `sso`, `mfa`,
`device_unlock`, and `other`;
- `requires_user_presence` and `saved_account_selected` booleans;
- start and end times on the capture clock;
- an outcome: `completed`, `cancelled`, `failed`, or `aborted`;
- the fixed list of suppressed source categories; and
- the timestamp, source ordinal, pixel digest, capture source, and optional
window geometry generation of the clean entry frame;
- for a normal close, the same proof fields for the fresh resume frame.

The API has no field for a provider name, account identifier, username,
password, passkey material, OTP, recovery code, vault item, or free-form note.
Method classes describe the handoff without describing the credential.

An autofill login fits this contract. Set `methods="password_manager"` and
`saved_account_selected=True`. The click on the saved account, the account
chooser, and any filled values stay inside the protected interval.

## Recorder API

The owner can control a recorder in the same process:

```python
from openadapt_capture import Recorder

with Recorder(
"./capture",
task_description="Download the monthly statement",
window={"owner": "Google Chrome", "title": "American Express"},
) as recorder:
if not recorder.wait_for_ready():
raise RuntimeError("Capture did not become ready")

handoff = recorder.begin_authentication(
methods="password_manager",
requires_user_presence=True,
saved_account_selected=True,
)

# The person selects the saved account and completes any OS or MFA prompt.
# Wait until the application no longer shows credential UI before ending.

marker = recorder.end_authentication(
handoff,
outcome="completed",
timeout=10,
)
assert marker.resume_frame is not None
```

`begin_authentication()` returns only after in-flight screen, input, window,
and structural operations finish. When audio is active, it also waits for the
microphone process to acknowledge suppression after its callback lock is clear.
The screen worker retains one clean entry frame before the open marker is
written. This closes the after-frame binding for an action that occurred just
before the handoff. New input stays blocked across that cut.

If that begin barrier times out or the open marker cannot be written, the
recorder stays protected and fails the recording. Resuming would create an
unmarked evidence gap.

`end_authentication()` first changes the interval to a resuming state. Input,
window metadata, structural data, and audio remain protected. The screen worker
then acquires and journals one new frame. For window-scoped recording, both the
entry and resume frames use the same source ordinal as their exact geometry.
Capture writes the resume proof to the sidecar before it reopens the other
sources.

If the timeout expires, the interval stays protected and the caller can repeat
the same end request. A source-frame error follows Capture's fail-loud media
rule and ends the recording. Capture never resumes input because a timer
expired or a frame failed.

## Authenticated process control

A launcher, Desktop, or Flow process can use the owner-only local control
channel:

```python
from openadapt_capture import (
begin_authentication_handoff,
end_authentication_handoff,
)

handoff = begin_authentication_handoff(
session_id=session_id,
methods=("password_manager", "mfa"),
requires_user_presence=True,
saved_account_selected=True,
)

# Complete the attended login, then close the protected interval.
marker = end_authentication_handoff(
handoff,
session_id=session_id,
outcome="completed",
)
```

These requests use the same loopback capability, process identity, message MAC,
clock bound, and replay checks as `status_recording()` and `stop_recording()`.
The begin client selects the interval UUID. A retry with the same UUID and the
same parameters returns the same handle. A retry with different parameters is
refused. End is idempotent for the same interval and outcome.

`RecorderStatus.authentication_protected` lets an owner show a local privacy
indicator. It does not expose method classes or account data.

## Stop and failure behavior

Stopping a recorder during a handoff closes the interval as `aborted`. It does
not take a terminal screenshot from the credential UI and does not claim a
resume frame. The rest of the capture can still finish and seal if its retained
evidence is valid.

Stopping during the entry cut, before the open marker exists, fails the
recording. Capture cannot seal a source gap that has no interval marker.

Nested handoffs are refused. An end call with the wrong handle is refused. A
method outside the fixed vocabulary is refused before the interval starts.
Current captures cannot contain an open interval at the completion seal.

`CaptureSession.load_verified()` checks each interval against the immutable
database. Both proofs must name retained screen rows with the same timestamps
and source ordinals. The entry proof must be the last pre-handoff frame. No
action, browser, window, or other screen row can occur inside the protected
range. Window-scoped proof must match the frame's capture source and geometry
generation. When PNG pixels are present, their source-pixel digest must match
the marker.

## Ownership boundary

Capture proves observation behavior. It proves that the named sources were not
retained and that a fresh frame existed before normal observation resumed.

Capture does not prove that the application accepted the login. The
`completed` outcome means that the owner completed the handoff. A compiler or
runtime must verify the application state before it treats the user as
authenticated. That contract belongs outside `openadapt-capture`.

Capture also does not store credentials or ask a password manager to release a
secret. Password managers, passkey providers, operating-system account choosers,
and MFA devices keep that authority. OpenAdapt can coordinate the attended
gate while those systems retain credential custody.

The native handoff does not turn the repository-only Chrome extension into a
supported recorder. The Playwright path must suppress DOM values, page frames,
network-derived observations, and browser input at its own source boundary.
The two recorders can share the schema after that browser contract exists.
54 changes: 46 additions & 8 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,23 @@ One recording has these stages:
2. Resolve the initial native-window or virtual-desktop coordinate scope.
3. Create the per-capture SQLite database and media staging path.
4. Observe native input and screen frames on separate workers.
5. Reserve each observation in one ordered source journal before any optional
5. Apply the optional authentication gate before source retention. An open gate
drops screen, input, structure, window data, browser-category data, and audio
content. A close waits for one fresh frame.
6. Reserve each observation in one ordered source journal before any optional
structural lookup. A failed reservation fails the session; later events
cannot pass it.
6. For a native window, enqueue each frame and its window geometry as one
7. For a native window, enqueue each frame and its window geometry as one
source-ordinal pair. Publish that geometry to input observers only after the
pair enters the journal.
7. Bind each actionable input to the last published frame pair and optional
8. Bind each actionable input to the last published frame pair and optional
structural observation. Retain one ordinal-later frame after input stops.
8. Stream RGB frames to a separately provisioned FFmpeg process.
9. Close, verify, and atomically promote the MP4. Retain an incomplete partial
9. Stream RGB frames to a separately provisioned FFmpeg process.
10. Close, verify, and atomically promote the MP4. Retain an incomplete partial
file on an encoder failure and never report it as complete media.
10. Post-process raw input into the public action view. A merged action keeps
11. Post-process raw input into the public action view. A merged action keeps
its terminal primitive's source binding and refuses mixed geometry epochs.
11. Reconcile committed rows with producer counts, verify the v2 frame/action
12. Reconcile committed rows with producer counts, verify the v2 frame/action
relations, inventory every immutable artifact, and write the completion
seal.

Expand Down Expand Up @@ -104,7 +107,42 @@ Each current window frame carries a process-bound window identity, display
topology digest, and geometry epoch digest. The window and screenshot rows use
the same source ordinal. An action uses a later ordinal and names the exact pair
that supplied its coordinates. Capture refuses process replacement, topology
drift, off-screen state, mixed generations, or a missing pair.
drift, a minimized or unproven off-screen state, mixed generations, or a
missing pair. A desktop-independent exact-window provider can capture an
occluded window or a window on another macOS Space.

## Authentication observation boundary

The default operating procedure authenticates before recording. Some workflows
need a login step, and long sessions can expire. Capture supports those cases
with a source-time protected interval.

The controller enters protection before it writes the open marker. It waits for
each in-flight source operation to finish. Native input holds that boundary from
receipt-time journal reservation through delivery, so a slow structural query
cannot cross the marker. Audio uses a process-shared suppression event and
inserts generated silence to preserve its clock. The parent waits for an audio
acknowledgement taken under the callback lock before it writes the start time.
Failure to reach this cut stops the recording. It cannot resume with an
unmarked gap.

Before the marker starts, the screen worker journals one clean entry frame.
This frame closes any pending after-frame relationship from normal input. New
input remains blocked while that frame enters the journal.

Normal close keeps all sources protected while the screen worker acquires a new
frame. The frame enters the ordered journal before the marker records its
source ordinal and pixel digest. A window-scoped frame also binds its geometry
generation. The controller persists the close marker before it reopens input.

`authentication-handoffs.json` is canonical JSON and part of the immutable
artifact inventory. The marker uses a fixed method vocabulary and accepts no
free text. A stopped interval becomes `aborted` and has no resume claim.

Capture proves suppression and frame reacquisition. It does not prove semantic
login success and does not hold credential authority. See
[`AUTHENTICATION_HANDOFF.md`](AUTHENTICATION_HANDOFF.md) for the API, schema,
control-channel behavior, and loader checks.

## Completion and consumer boundary

Expand Down
Loading
Loading