diff --git a/openspec/changes/add-desktop-local-remote-modes/.openspec.yaml b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/.openspec.yaml similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/.openspec.yaml rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/.openspec.yaml diff --git a/openspec/changes/add-desktop-local-remote-modes/README.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/README.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/README.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/README.md diff --git a/openspec/changes/add-desktop-local-remote-modes/design.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/design.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/design.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/design.md diff --git a/openspec/changes/add-desktop-local-remote-modes/proposal.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/proposal.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/proposal.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/proposal.md diff --git a/openspec/changes/add-desktop-local-remote-modes/specs/auth/spec.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/auth/spec.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/specs/auth/spec.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/auth/spec.md diff --git a/openspec/changes/add-desktop-local-remote-modes/specs/desktop-app/spec.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/desktop-app/spec.md similarity index 71% rename from openspec/changes/add-desktop-local-remote-modes/specs/desktop-app/spec.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/desktop-app/spec.md index f03df87..2e83312 100644 --- a/openspec/changes/add-desktop-local-remote-modes/specs/desktop-app/spec.md +++ b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/desktop-app/spec.md @@ -3,17 +3,17 @@ ### Requirement: Native desktop build The system SHALL be buildable as a native desktop application from the existing -web application, producing a self-contained binary per platform whose UI runs in -the operating system's webview and whose server logic runs in-process. The -desktop build SHALL NOT fork or duplicate the domain service layer. The existing -container image and server deployment SHALL remain a supported, unchanged -output. +web application, producing a self-contained distributable artifact per platform +whose UI runs in an embedded webview and whose server logic runs in-process. +The desktop build SHALL NOT fork or duplicate the domain service layer. The +existing container image and server deployment SHALL remain a supported, +unchanged output. #### Scenario: Desktop artifact produced - **WHEN** the desktop build is run for a platform -- **THEN** it produces a single distributable binary for that platform that runs - the application without a separately installed runtime or browser +- **THEN** it produces a self-contained distributable artifact for that platform + that runs the application without a separately installed runtime or browser #### Scenario: Server deployment unchanged @@ -27,8 +27,9 @@ On first launch, the desktop application SHALL require the user to choose between Local mode ("just me, on this computer") and Remote mode ("I have a Quantum server"). The choice SHALL be persisted outside the application database. The application SHALL NOT present an in-app toggle to switch modes; -changing mode SHALL be an explicit reset action that clears the persisted choice -and returns the user to the mode-selection screen on the next launch. +changing mode SHALL be an explicit Disconnect action that clears the persisted +choice and returns the user to the mode-selection screen immediately, deleting +no data. #### Scenario: Mode chosen on first launch @@ -41,11 +42,12 @@ and returns the user to the mode-selection screen on the next launch. - **WHEN** a user has chosen a mode and relaunches the app - **THEN** the app starts directly in the chosen mode without asking again -#### Scenario: Reset returns to selection +#### Scenario: Disconnect returns to selection -- **WHEN** a user performs the reset action -- **THEN** the persisted mode is cleared and the next launch presents the - mode-selection screen again +- **WHEN** a user performs the Disconnect action +- **THEN** the persisted mode is cleared and the mode-selection screen is + presented, immediately and on every launch until a mode is chosen again, + with local data left on disk ### Requirement: Local mode runtime @@ -100,12 +102,12 @@ agent host. A local agent request without a valid token SHALL be rejected. ### Requirement: Remote mode thin client In Remote mode, the application SHALL act as a native client to an existing -Quantum server: it SHALL point its webview at the server's origin and SHALL NOT -run an embedded server or a local database. Authentication SHALL use the -server's normal cookie login run inside the webview (specified in the auth -capability). The application SHALL NOT register itself as an ATProto OAuth -client, and SHALL NOT expose a local MCP mount — MCP in Remote mode is served by -the user's server. +Quantum server: it SHALL point its webview at the server's origin, SHALL NOT +serve application content locally, and SHALL NOT open a local database. +Authentication SHALL use the server's normal cookie login run inside the +webview (specified in the auth capability). The application SHALL NOT register +itself as an ATProto OAuth client, and SHALL NOT expose a local MCP mount — MCP +in Remote mode is served by the user's server. #### Scenario: Remote mode targets an existing server @@ -113,8 +115,8 @@ the user's server. - **THEN** the app presents that server's interface and stores no ledger data locally -#### Scenario: Remote mode runs no local server +#### Scenario: Remote mode serves nothing locally - **WHEN** the app is running in Remote mode -- **THEN** it has not started an embedded server, opened a local database, or - mounted a local MCP listener +- **THEN** every local request is handed to the server's origin, no local + database has been opened, and no local MCP listener is mounted diff --git a/openspec/changes/add-desktop-local-remote-modes/specs/simplefin-sync/spec.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/simplefin-sync/spec.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/specs/simplefin-sync/spec.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/specs/simplefin-sync/spec.md diff --git a/openspec/changes/add-desktop-local-remote-modes/tasks.md b/openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/tasks.md similarity index 100% rename from openspec/changes/add-desktop-local-remote-modes/tasks.md rename to openspec/changes/archive/2026-07-23-add-desktop-local-remote-modes/tasks.md diff --git a/openspec/specs/auth/spec.md b/openspec/specs/auth/spec.md index 82290c8..6441ce1 100644 --- a/openspec/specs/auth/spec.md +++ b/openspec/specs/auth/spec.md @@ -10,13 +10,16 @@ archive. ### Requirement: ATProto OAuth login The system SHALL authenticate users via AT Protocol OAuth using handle-based -login. The user enters their handle (or DID); the system resolves it, performs -the OAuth authorization flow (PAR, PKCE, DPoP) against the user's authorization -server, and establishes an application session on success. The system SHALL -request only the `atproto` scope and SHALL NOT make authenticated requests to -the user's PDS after authentication. Resolving and rendering profile pictures -from public ATProto profile data — without using the OAuth session or any -application credential — is permitted. +login when running as a server. The user enters their handle (or DID); the +system resolves it, performs the OAuth authorization flow (PAR, PKCE, DPoP) +against the user's authorization server, and establishes an application session +on success. The system SHALL request only the `atproto` scope and SHALL NOT make +authenticated requests to the user's PDS after authentication. Resolving and +rendering profile pictures from public ATProto profile data — without using the +OAuth session or any application credential — is permitted. When the application +runs as a desktop build in Local mode, it SHALL NOT perform this login flow and +SHALL NOT initialize an ATProto OAuth client; authentication is governed instead +by the local single-user runtime. #### Scenario: Successful login with allowlisted handle @@ -31,6 +34,12 @@ application credential — is permitted. - **THEN** the system shows an error on the login page without starting the OAuth flow +#### Scenario: Local mode performs no login + +- **WHEN** the application runs as a desktop build in Local mode +- **THEN** no login page is presented, no OAuth client is initialized, and + requests are authenticated as the local single user + ### Requirement: DID allowlist authorization The system SHALL authorize users solely by membership in the `ALLOWED_DIDS` @@ -157,3 +166,65 @@ textual handle presentation. - **WHEN** pages containing avatars are rendered - **THEN** all profile-image requests originate from the browser against public endpoints, and the application server performs no profile-data requests + +### Requirement: Local single-user runtime + +When the application runs as a desktop build in Local mode, it SHALL operate as +a single-user system with no login. It SHALL seed one synthetic user with the +DID `did:local:self` and a user-chosen display name, and SHALL treat that user +as the authenticated principal for every request, never redirecting to a login +page. This runtime SHALL be reachable only when the application is explicitly +configured for Local mode; a server deployment SHALL NOT enable it, and +supplying server authentication configuration together with Local mode SHALL be +a configuration error rather than a silent relaxation. All existing per-actor +invariants (such as manual categorization recording an actor) SHALL be satisfied +by the synthetic user without special-casing. + +#### Scenario: Local user authenticated without a session + +- **WHEN** any request is handled in Local mode +- **THEN** it is authenticated as `did:local:self` with the chosen display name, + and no session cookie or login is required + +#### Scenario: Display name drives provenance + +- **WHEN** the local user manually categorizes a transaction +- **THEN** the categorization records the local user as actor and provenance + displays the chosen display name + +#### Scenario: Local relaxation cannot apply to a server + +- **WHEN** a deployment is configured for Local mode but also supplies + `ALLOWED_DIDS` or OAuth signing configuration +- **THEN** startup fails with a configuration error rather than running a server + without login + +### Requirement: Remote client cookie login + +When the application runs as a desktop build in Remote mode, it SHALL +authenticate against an existing Quantum server using that server's normal +cookie-based login, run inside the app's webview exactly as in a browser. The +application SHALL navigate the webview to the configured server origin and SHALL +NOT itself perform, register for, or hold ATProto OAuth credentials — the server +remains the sole OAuth client, and the app is only a webview that follows the +server's redirects and carries the resulting session cookie. The application +SHALL NOT require a custom URL scheme, a deep link, or a bearer-token handoff. + +#### Scenario: Remote client logs in through the webview + +- **WHEN** a Remote-mode user opens the app pointed at their server and is not + yet authenticated +- **THEN** the server's login runs in the webview, the ATProto flow completes, + and the server sets its session cookie, after which the app is authenticated + +#### Scenario: Session cookie authenticates subsequent requests + +- **WHEN** the Remote-mode webview holds a valid session cookie +- **THEN** every request it makes to the server carries that cookie and is + authenticated, with no bearer token and no app-side OAuth + +#### Scenario: App is not an OAuth client + +- **WHEN** the Remote-mode app authenticates a user +- **THEN** it registers no custom URL scheme and holds no OAuth credential; the + server performs the entire OAuth flow diff --git a/openspec/specs/desktop-app/spec.md b/openspec/specs/desktop-app/spec.md new file mode 100644 index 0000000..e3f984c --- /dev/null +++ b/openspec/specs/desktop-app/spec.md @@ -0,0 +1,136 @@ +# desktop-app Specification + +## Purpose + +Package the existing Quantum web application as a native desktop app with two +modes chosen at first launch: Local mode, a single-user runtime with no login +that stores its database in the operating system's application-data directory +and runs the embedded server and a loopback-only MCP listener in-process; and +Remote mode, a thin webview client onto a self-hosted Quantum server that uses +the server's normal cookie login and serves nothing locally. The mode choice is +persisted outside the database and changed only via an explicit Disconnect +action. The domain service layer is shared, not forked, and the container-based +server deployment remains a supported, unchanged output. + +## Requirements + +### Requirement: Native desktop build + +The system SHALL be buildable as a native desktop application from the existing +web application, producing a self-contained distributable artifact per platform +whose UI runs in an embedded webview and whose server logic runs in-process. +The desktop build SHALL NOT fork or duplicate the domain service layer. The +existing container image and server deployment SHALL remain a supported, +unchanged output. + +#### Scenario: Desktop artifact produced + +- **WHEN** the desktop build is run for a platform +- **THEN** it produces a self-contained distributable artifact for that platform + that runs the application without a separately installed runtime or browser + +#### Scenario: Server deployment unchanged + +- **WHEN** the container image is built and deployed as before +- **THEN** it behaves identically to prior releases, with no dependency on the + desktop build + +### Requirement: First-launch mode selection + +On first launch, the desktop application SHALL require the user to choose +between Local mode ("just me, on this computer") and Remote mode ("I have a +Quantum server"). The choice SHALL be persisted outside the application +database. The application SHALL NOT present an in-app toggle to switch modes; +changing mode SHALL be an explicit Disconnect action that clears the persisted +choice and returns the user to the mode-selection screen immediately, deleting +no data. + +#### Scenario: Mode chosen on first launch + +- **WHEN** the desktop app is launched for the first time +- **THEN** it presents the Local/Remote choice and does not proceed to the app + until one is chosen + +#### Scenario: Choice persisted across launches + +- **WHEN** a user has chosen a mode and relaunches the app +- **THEN** the app starts directly in the chosen mode without asking again + +#### Scenario: Disconnect returns to selection + +- **WHEN** a user performs the Disconnect action +- **THEN** the persisted mode is cleared and the mode-selection screen is + presented, immediately and on every launch until a mode is chosen again, + with local data left on disk + +### Requirement: Local mode runtime + +In Local mode, the application SHALL run its embedded server configured for +local operation: it SHALL NOT require or validate `APP_URL`, `ALLOWED_DIDS`, or +OAuth signing configuration, and SHALL NOT initialize an ATProto OAuth client. +The application database SHALL be stored in the operating system's +application-data directory by default. On first launch in Local mode, the +application SHALL prompt for a display name and SHALL seed a single synthetic +user for it. Connecting a bank via a SimpleFIN setup token SHALL work in Local +mode exactly as on a server. + +#### Scenario: Local mode boots without server configuration + +- **WHEN** the app starts in Local mode with no `APP_URL`, `ALLOWED_DIDS`, or + OAuth key configured +- **THEN** it starts successfully, initializing no OAuth client and requiring no + login + +#### Scenario: Local database in app-data directory + +- **WHEN** Local mode initializes its database for the first time +- **THEN** the SQLite file is created under the operating system's + application-data directory + +#### Scenario: Bank connection in local mode + +- **WHEN** a Local-mode user pastes a valid SimpleFIN setup token in Settings +- **THEN** the connection is claimed and synced exactly as on a server, with no + login required + +### Requirement: Local mode agent access + +In Local mode, the application SHALL expose the MCP server on a loopback-only +listener, using the same MCP handler and requiring the same bearer API token as +the server route. The application MAY write the loopback endpoint and a minted +token to a well-known application-data location to simplify configuring a local +agent host. A local agent request without a valid token SHALL be rejected. + +#### Scenario: Local agent reaches MCP over loopback + +- **WHEN** an agent on the same machine connects to the loopback MCP endpoint + with a valid token +- **THEN** it can list and call the same tools the server route exposes + +#### Scenario: Loopback MCP still requires a token + +- **WHEN** a process on the machine connects to the loopback MCP endpoint + without a valid token +- **THEN** the request is rejected + +### Requirement: Remote mode thin client + +In Remote mode, the application SHALL act as a native client to an existing +Quantum server: it SHALL point its webview at the server's origin, SHALL NOT +serve application content locally, and SHALL NOT open a local database. +Authentication SHALL use the server's normal cookie login run inside the +webview (specified in the auth capability). The application SHALL NOT register +itself as an ATProto OAuth client, and SHALL NOT expose a local MCP mount — MCP +in Remote mode is served by the user's server. + +#### Scenario: Remote mode targets an existing server + +- **WHEN** a user selects Remote mode and provides their Quantum server address +- **THEN** the app presents that server's interface and stores no ledger data + locally + +#### Scenario: Remote mode serves nothing locally + +- **WHEN** the app is running in Remote mode +- **THEN** every local request is handed to the server's origin, no local + database has been opened, and no local MCP listener is mounted diff --git a/openspec/specs/simplefin-sync/spec.md b/openspec/specs/simplefin-sync/spec.md index c5a6680..cf36698 100644 --- a/openspec/specs/simplefin-sync/spec.md +++ b/openspec/specs/simplefin-sync/spec.md @@ -31,20 +31,37 @@ expected initially. ### Requirement: Scheduled and manual sync -The system SHALL sync each connection once daily via a scheduled job and SHALL -provide a manual "sync now" action in the UI. A sync fetches +The system SHALL sync each connection automatically on a schedule appropriate to +how it is deployed, and SHALL provide a manual "sync now" action in the UI. A +server deployment SHALL sync each connection once daily via a scheduled job. A +desktop build in Local mode, which is not continuously running, SHALL instead +sync on application launch — catching up data missed while the app was closed — +and periodically while the application runs, rather than relying on a fixed-time +schedule that has no catch-up for missed fires. In all cases a sync fetches `GET {access_url}/accounts` including pending transactions and a start date that safely overlaps previously fetched data. -#### Scenario: Daily scheduled sync +#### Scenario: Daily scheduled sync on a server -- **WHEN** the daily schedule fires +- **WHEN** the daily schedule fires on a server deployment - **THEN** the system performs a sync for every connection and records the outcome +#### Scenario: Sync on launch in local mode + +- **WHEN** a desktop Local-mode application starts +- **THEN** it performs a sync for every connection, bringing data current after + any period the app was closed + +#### Scenario: Periodic sync while running in local mode + +- **WHEN** a desktop Local-mode application has been running for the configured + interval +- **THEN** it performs another sync for every connection + #### Scenario: Manual sync -- **WHEN** an authenticated user triggers "sync now" +- **WHEN** a user triggers "sync now" - **THEN** a sync runs immediately and the UI reflects the result #### Scenario: Sync failure