diff --git a/README.md b/README.md index 86108e1..f99c393 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,37 @@ # openadapt-tray -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) [![PyPI version](https://img.shields.io/pypi/v/openadapt-tray.svg)](https://pypi.org/project/openadapt-tray/) +[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -> **Lifecycle: Experimental supporting surface.** The canonical compiler and -> governed runtime live in -> [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow). This tray -> is published on PyPI as `openadapt-tray`, but it is a companion status -> surface, not a generally available integrated desktop product. - -## What OpenAdapt is - -OpenAdapt is a governed demonstration compiler. You record a workflow once, it -compiles the demonstration into a deterministic program, and it replays that -program with **zero model calls on the healthy path**. When an interface drifts, -OpenAdapt re-resolves from retained evidence or proposes a governed repair, and -it **halts instead of guessing** when verification fails. Substrates are all -first-class in the product design (web, Windows, macOS, Linux, RDP, and -Citrix/VDI), with browser proven end to end today and the other substrates at -earlier maturity. That workflow logic belongs to `openadapt-flow`. - -## What the tray is - -OpenAdapt Tray is a lightweight system-tray companion for the OpenAdapt desktop -authoring experience. It does not record, compile, replay, repair, or train -anything itself. It mirrors state and hands local actions to a companion desktop -process over an authenticated loopback connection, and it reads a small -needs-attention count from the hosted control plane. - -From the tray you can: - -- **Start or stop recording** on the desktop app, and see the live recording - and compile lifecycle. -- **Open recent captures** from the local captures directory. -- **See how many automations need attention** and jump straight to them. -- **Open the desktop app** or the **cloud dashboard**. -- **Pause or resume sync**, shown as its own channel. -- **Sign in** (open the ingest-token settings page) and open **settings**. - -### The per-state OpenAdapt-mark icon - -The tray icon is always the OpenAdapt mark, tinted per lifecycle state so the -state stays readable at a glance: - -| State | Tint | -| --- | --- | -| Idle | Brand blue | -| Recording starting | Amber | -| Recording | Red | -| Recording stopping | Amber | -| Compiling | Purple | -| Error | Red | - -The mark is stored once as a high-resolution transparent master and tinted from -it, so every state renders as the same recognizable mark and the states never -drift apart. Recording lifecycle and sync are modelled as two independent -channels: a machine can be compiling a fresh recording while a previous push is -still syncing, or sit idle while offline. - -## How it fits with desktop and cloud +A menu-bar icon that shows what OpenAdapt is doing and gives you a start/stop +button for it. It records nothing, compiles nothing, and replays nothing. It +mirrors state from the desktop app over an authenticated loopback socket and +reads one number from the hosted control plane. -```text -openadapt-tray (status mirror + launcher) - | \ - | authenticated loopback IPC \ HTTPS: GET needs-attention count - v v -openadapt-desktop (local cockpit) hosted control plane (app.openadapt.ai) - | - v -openadapt-flow (compile / replay / halt / repair / teach) -``` +Which means the honest description is: this is a status surface, not an +integrated desktop product. The workflow logic all lives in +[`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow). -- **Local control** goes to `openadapt-desktop` over an authenticated loopback - socket. The desktop app writes a discovery file the tray reads, then the tray - sends start/stop, open-library, open-teach, and pause/resume-sync commands and - consumes desktop status events. -- **Hosted status** is a narrow read: the tray polls a needs-attention count and - routes a click to the right place. It never uploads screenshots, bundles, or - capture artifacts. +[Documentation](https://docs.openadapt.ai) · +[openadapt-desktop](https://github.com/OpenAdaptAI/openadapt-desktop) · +[OpenAdapt launcher](https://github.com/OpenAdaptAI/OpenAdapt) -## Release boundary +## Install -- The hosted-lifecycle behavior described here is merged on `main` and published - to PyPI; the package classifier is Pre-Alpha. -- Unit tests cover the client state machine, IPC framing, menus, the per-state - icon tinting, and mocked hosted HTTP behavior. They do not prove a working - desktop installer, a live hosted service, or an end-to-end authoring loop. -- The `openadapt-desktop` `main` branch now serves the exact discovery-socket - and command contract this tray expects, but the two surfaces have - not been validated together end to end, and no signed, generally available - desktop build ships that server yet. Treat the tray as a status surface until - that integration is qualified. +```bash +pip install openadapt-tray +openadapt-tray # openadapt-tray-gui is the same entry point, + # registered under gui_scripts so Windows + # launches it without a console window +``` -The retired model-training controls and training states are not part of this -release. +Needs a graphical session. With no desktop IPC server and no hosted token it +will sit there offline, which is correct behaviour rather than a failure. -## Expected menu +## The menu -The menu is built from current local and hosted state: +Built from whatever local and hosted state currently exists: ```text Start Recording () @@ -112,30 +45,40 @@ Settings... Quit ``` -The account row changes to a sign-in action, an expiry warning, or an -unavailable status. Selecting it opens the credential settings page. - -During a local operation, the recording item changes to Starting, Stop -Recording, Stopping, or Compiling. These labels reflect events; the tray does -not perform the work. +During a local operation the recording item becomes Starting, Stop Recording, +Stopping, or Compiling. The account row turns into a sign-in action, an expiry +warning, or an unavailable status, and clicking it opens the credential +settings page. These labels follow events; the tray isn't doing the work. -## Integration contract +The icon is always the OpenAdapt mark, tinted by state so you can read it at a +glance: brand blue idle, amber starting or stopping, red while recording, +purple while compiling, red on error. One high-resolution transparent master +gets tinted, so the states can't drift into different-looking marks. -### Local desktop IPC +Recording and sync are two independent channels. A machine can compile a fresh +recording while a previous push is still syncing, or sit idle and offline. -The tray discovers a local service from `~/.openadapt/desktop_ipc.json`, then -uses an authenticated loopback connection. It can send commands to start or stop -recording, open the workflow library or teach surface, and pause or resume sync. -It also consumes desktop status events. +## How it talks to things -If discovery fails, the tray launches `openadapt-desktop` and waits about ten -seconds for the service. The desktop `main` branch now implements a matching -token-authenticated loopback server and writes this discovery file, but that -path has not yet been validated end to end from a shipped desktop build. +```text +openadapt-tray (status mirror + launcher) + | \ + | authenticated loopback IPC \ HTTPS: GET needs-attention count + v v +openadapt-desktop (local cockpit) hosted control plane (app.openadapt.ai) + | + v +openadapt-flow (compile / replay / halt / repair / teach) +``` -### Hosted needs-attention polling +Local control goes to `openadapt-desktop`. The desktop app writes a discovery +file at `~/.openadapt/desktop_ipc.json`, the tray reads it and opens an +authenticated loopback connection, then sends start/stop, open-library, +open-teach, and pause/resume-sync, and consumes status events coming back. If +discovery fails, the tray launches `openadapt-desktop` and waits about ten +seconds. -The poller calls: +Hosted state is one narrow read: ```text GET /api/needs-attention/count @@ -149,70 +92,47 @@ Authorization: Bearer } } ``` -The token is resolved from `OPENADAPT_INGEST_TOKEN` or the OS keychain and is -not written to `tray.json`. The default poll interval is 60 seconds, clamped to -at least 30 seconds, with a slower offline retry. - -The control plane decides when the credential enters its 14-day warning -window. The tray shows one actionable notification for each credential and -expiry, including after a tray restart. It stores only a non-secret identity -digest for notification deduplication. It never stores or logs the token. - -This is a narrow status endpoint, not hosted execution. The tray does not upload -screenshots, workflow bundles, or capture artifacts through this poller. - -### Deployment-lane routing - -- `cloud`: a needs-attention click opens `/dashboard`, which lists - open halts and uncertain dispatches. -- `byoc`: while desktop IPC is connected, the click sends `open_teach` locally - so workflow data can remain in the customer environment. -- `byoc` without desktop IPC: the current implementation falls back to the - hosted dashboard. Regulated deployments must not treat this fallback as a - validated PHI-safe path; it should be changed or policy-gated before - production use. - -## Installation - -```bash -pip install openadapt-tray -openadapt-tray # run the tray application -``` - -Treat the install as an Experimental status surface, not a production desktop -product (see the release boundary above). - -## Development quickstart - -```bash -git clone https://github.com/OpenAdaptAI/openadapt-tray.git -cd openadapt-tray - -uv sync --extra dev -uv run pytest tests -q -uv run openadapt-tray -``` - -Running the process requires a graphical desktop/session. Without a compatible -desktop IPC server or hosted token it may correctly remain offline, fail local -actions, or open only browser routes. - -For a runnable OpenAdapt workflow, use the canonical launcher separately: - -```bash -pip install 'openadapt[browser]' -openadapt quickstart -``` +The token comes from `OPENADAPT_INGEST_TOKEN` or the OS keychain, never from +`tray.json`, and it's never stored or logged. Polling defaults to 60 seconds, +clamps to a 30-second floor, and backs off when offline. The control plane +decides when a credential enters its 14-day warning window; the tray shows one +actionable notification per credential and expiry, surviving a tray restart, +and keeps only a non-secret identity digest to deduplicate. + +No screenshots, bundles, or capture artifacts go through this poller. + +Clicking the needs-attention row routes by lane. On `cloud` it opens +`/dashboard`. On `byoc` with desktop IPC connected it sends +`open_teach` locally, so workflow data stays inside the customer environment. +On `byoc` with no desktop IPC it currently falls back to the hosted dashboard, +and a regulated deployment should not treat that fallback as a validated +PHI-safe path. Gate it or change it before production. + +## What this doesn't do yet + +The tray's client behaviour is covered by unit tests: the state machine, the +IPC framing, the menus, the icon tinting, and mocked hosted HTTP. None of that +proves a working installer, a live hosted service, or an authoring loop that +runs end to end. + +- `openadapt-desktop`'s `main` serves the exact discovery-socket and command + contract this tray expects, but the two have not been validated together end + to end, and no signed generally-available desktop build ships that server. +- No packaged installer proves tray startup, permissions, or auto-start on + macOS, Windows, and Linux. +- Hosted polling is tested against mocks. Nothing here validates a live service + contract. +- Recent-capture View still calls a legacy launcher command before falling back + to a file browser. +- Sign-in opens a settings page. There's no interactive authentication. +- The tray doesn't certify a workflow or verify its effects. Nothing here is a + safety control. ## Configuration -Non-secret settings are stored at: - -- macOS: `~/Library/Application Support/openadapt/tray.json` -- Windows: `%APPDATA%/openadapt/tray.json` -- Linux: `${XDG_CONFIG_HOME:-~/.config}/openadapt/tray.json` - -Representative settings: +Non-secret settings live at `~/Library/Application Support/openadapt/tray.json` +on macOS, `%APPDATA%/openadapt/tray.json` on Windows, and +`${XDG_CONFIG_HOME:-~/.config}/openadapt/tray.json` on Linux: ```json { @@ -231,25 +151,20 @@ Representative settings: } ``` -`deployment_lane` accepts `cloud` or `byoc`. The ingest token does not belong in -this file. +`deployment_lane` takes `cloud` or `byoc`. The triple-ctrl stop hotkey is real, +not a typo: the `stop_on_triple_ctrl` setting defaults to true and is what +enables it. The ingest token does +not belong in this file. -## Known gaps +## Development -- The desktop socket contract exists on the desktop `main` branch, but no - shipped desktop build has been validated end to end with this tray. -- No packaged installer proves tray startup, permissions, or auto-start across - macOS, Windows, and Linux. -- Hosted polling is tested with mocks; repository tests do not validate a live - service contract or service-level commitments. -- BYOC fallback can open the hosted dashboard when desktop IPC is absent. -- Recent-capture View still invokes a legacy launcher command before its - file-browser fallback. -- Login opens an ingest-token settings page; the tray does not implement - interactive authentication. -- The tray does not certify workflow safety or verify workflow effects. - -## Project structure +```bash +git clone https://github.com/OpenAdaptAI/openadapt-tray.git +cd openadapt-tray +uv sync --extra dev +uv run pytest tests -q +uv run openadapt-tray +``` ```text src/openadapt_tray/ @@ -261,20 +176,15 @@ src/openadapt_tray/ icons.py per-state OpenAdapt-mark tinting keychain.py ingest-token lookup config.py non-secret local preferences -tests/ mocked/unit coverage for these client boundaries ``` -## Related projects - -| Project | Lifecycle and role | -| --- | --- | -| [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow) | Canonical workflow compiler, runtime, certification, and governed repair engine | -| [`openadapt-desktop`](https://github.com/OpenAdaptAI/openadapt-desktop) | Experimental authoring/teaching cockpit; its `main` now serves the IPC contract this tray expects, pending end-to-end validation | -| [`OpenAdapt`](https://github.com/OpenAdaptAI/OpenAdapt) | Flagship launcher and meta-repository | +For an OpenAdapt workflow you can actually run, use the launcher instead: -Documentation for the wider stack lives at -[docs.openadapt.ai](https://docs.openadapt.ai). +```bash +pip install 'openadapt[browser]' +openadapt quickstart +``` ## License -MIT. See [LICENSE](LICENSE). +[MIT](LICENSE) diff --git a/tests/test_public_metadata.py b/tests/test_public_metadata.py index 6d86187..035b0aa 100644 --- a/tests/test_public_metadata.py +++ b/tests/test_public_metadata.py @@ -7,16 +7,18 @@ def test_public_metadata_identifies_unreleased_supporting_surface() -> None: - readme = (ROOT / "README.md").read_text() + # Collapse wrapping so an assertion matches the sentence rather than the + # line breaks a reflow happens to leave behind. + readme = re.sub(r"\s+", " ", (ROOT / "README.md").read_text()) pyproject = (ROOT / "pyproject.toml").read_text() - assert "Lifecycle: Experimental supporting surface" in readme - # The hosted lifecycle shipped on PyPI and the openadapt-desktop main branch - # now serves the matching IPC contract, but the two surfaces have not been - # validated together end to end and no signed desktop build ships that - # server. The README must keep saying so rather than implying an integrated, - # generally available product. - assert "Release boundary" in readme + # The README must keep saying that this package is a status surface rather + # than an integrated, generally available desktop product, and it must keep + # saying that the tray and openadapt-desktop have not been proven together. + # These assert the claims, not one particular heading, so the wording can + # improve without weakening the guard. + assert "status surface, not an integrated desktop product" in readme + assert "records nothing, compiles nothing, and replays nothing" in readme assert "not been validated together end to end" in readme assert "openadapt-flow" in readme assert "Development Status :: 2 - Pre-Alpha" in pyproject