diff --git a/docs/adr/0001-credentials-on-disk.md b/docs/adr/0001-credentials-on-disk.md index b7a2cd73..9cf21e5b 100644 --- a/docs/adr/0001-credentials-on-disk.md +++ b/docs/adr/0001-credentials-on-disk.md @@ -1,6 +1,6 @@ # Credentials at rest are revocable tokens on disk under ~/.amico — passwords are never persisted -Status: accepted (2026-07-19) +Status: accepted (2026-07-19) · amended for Pasqal Cloud (2026-07-21, see addendum) Connection credentials are stored as `0600` JSON files under `~/.amico/`. The decided write discipline for the new storage seam is **atomic with the mode set at file birth** — the tmp file is created `0600` before rename, because the default write mode is not owner-only (`0o666 & ~umask`, typically `0644`). This is new work: no such write path exists yet (the repo's atomic-write helper takes no mode argument, and the in-flight key-entry command writes `cloud.json` mode-at-birth but non-atomically — it migrates onto the seam after its branch merges; the migration is scoped into the Connections-panel issue). **Only revocable tokens are ever at rest.** For Company Compute that is the service token (`cloud.json` — shape and location unchanged; its writers adopt the seam's discipline). For Pasqal Cloud (`pasqal.json`: `{project_id, token}` plus expiry metadata; exact shape finalized against the SDK in the implementing issue), the user's username/password are used **in memory only** at key-entry time — for the validation handshake and to mint an Auth0 token via the SDK's token-provider mechanism — and only the token is persisted; runtime authentication uses the stored token through a `TokenProvider`. If token refresh proves unsupported against the SDK, the fallback is **session-only** (re-prompt, token held in process memory) — never password-on-disk. Pasqal's own SDK documentation warns against persisting passwords. @@ -11,3 +11,13 @@ Connection credentials are stored as `0600` JSON files under `~/.amico/`. The de **Accepted residuals:** a stolen token file grants access until expiry/revocation (bounded by `0600` + the same-user trust model); token expiry can surface a "reconnect" prompt when no refresh path exists. Each connection type reads/writes through a single storage-seam interface, so a later **OS-keychain backend reachable from the server process** (e.g. shelling to the platform keychain CLI) remains a contained swap. VSCode SecretStorage specifically is *not* a contained swap — it is reachable only from the extension host, and per ADR 0002's flip condition a SecretStorage migration would flip the seam itself. This resolves the open question "where the extension stores it" in the cloud-solve-service team spec of 2026-06-28 (team vault, armonissima specs). + +## Addendum (2026-07-21): Pasqal password moves to the OS keychain — evidence-forced + +**What changed.** For the Pasqal Cloud connection ONLY, the username+password are stored in the **OS keychain** (macOS login Keychain / Linux Secret Service; never a plaintext file), and the fork server silently re-runs the password grant when the ~24h access token lapses. The token continues to persist in `pasqal.json` (`0600`, atomic, mode-at-birth) so every token consumer — the `amico-pasqal` launcher contract above all — is unchanged. Disconnect wipes both the keychain slots and the token file. Everything else in this ADR stands, including Company Compute (service token, no password ever exists). + +**Why the original stance broke.** This ADR's no-password design assumed at least one of: a mintable long-lived token, a working refresh path, or a tolerable re-prompt cadence. Live-API evidence (Jack's session of 2026-07-21, `pasqal-auth-bundle`; independently probed the same day) eliminated all three: (1) authorization-code/PKCE cannot complete — Pasqal's public client registers no localhost callback and we do not own the tenant; (2) the device-code grant is disabled on the client; (3) **refresh tokens are issued but unusable** — the API returns 403 for any access token whose `gty` claim includes `refresh_token`, with otherwise identical `aud`/`scope`/`permissions` to a working password-grant token (deliberate grant-type gating); (4) access tokens live ~24h. Net: for a user account, a valid token cannot exist for more than ~24h without the password being present somewhere. The named fallback (session-only re-prompt) would mean typing the password roughly daily; product decision (Kate, 2026-07-21) is that this friction is not acceptable for the panel, adopting Jack's keychain interim. + +**Revised risk position.** The keychain is same-user-readable, so a local process running as the user can obtain the password — the exact blast radius this ADR originally refused. Accepted knowingly because Pasqal's design caps what any client can do: the password must be *presentable* to keep working at all. Mitigations: keychain-only (no plaintext file fallback), per-connection slots, disconnect wipes, and the panel states plainly that the password is saved to the system keychain. **Backend (per Jack's validated `pasqalAuth.ts`, adopted by Kate):** the OS keychain is reached via `@napi-rs/keyring` (macOS login Keychain / Linux Secret Service / Windows Credential Manager) — the same native module Jack's standalone session used, joining node-pty and tree-sitter as native deps already carried in the bun-compiled fork binary. Where the native binding cannot load (headless Linux with no Secret Service daemon), the store degrades to **session-memory** — password held in the server process only, re-prompt after restart — which is this ADR's original named fallback, never a plaintext file. (An earlier draft of this addendum proposed shelling to the platform CLI; superseded by Jack's native-module choice.) + +**Exit condition.** A Pasqal **service account** (`client_credentials`) retires the password entirely — client id+secret are revocable-credential class and fit this ADR's original at-rest rules with no exception. Request is with Pasqal (Baptiste Peyras, project `Harmoniqs Tests`); the day it is provisioned, the keychain password slots are deleted and this addendum's exception ends. Until then this exception is scoped to `pasqal-cloud` and must not be cited as precedent for any other connection.