You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Pasqal Cloud credentials currently enter Amico through chat — the password rides the agent's command line (ps-visible, transcript-persisted), exactly the exposure ADR 0002 calls out. The Connections panel design needs its auth flow implemented, and the resolved design is browser sign-in: the password never enters Amico at all.
Approach
OAuth authorization-code + PKCE with a loopback callback (RFC 8252 — the gh auth login / gcloud pattern), implemented as an amico pasqal connect CLI verb so the fork server's Connections routes reuse it via the established spawn-a-CLI-child pattern, and headless/opencode serve surfaces get the identical flow for free. Sign-in happens on Pasqal's own Auth0 page (password manager / SSO / MFA native); Amico receives a single-use code on a 127.0.0.1 listener, exchanges it in-process for a revocable JWT access token, and persists token-only per ADR 0001.
Approaches Considered
Browser handoff via CLI verb + loopback callback — chosen: never touches the password, CLI parity, fits the fork's validator-child pattern.
In-panel username/password form, server-side password-grant mint (ADR 0001 original) — works today with zero Pasqal changes, but the password transits Amico memory; retained only as documented fallback if Pasqal declines the client toggle.
vscode:// URI-handler deep link back to the extension — rejected: extension-only surface, breaks opencode serve/CLI parity (ADR 0002's seam rationale).
Device-code grant — deferred, not rejected: better for SSH (no callback), but currently disabled on Pasqal's public client; becomes a follow-up slice if enabled.
Scope
In: PKCE + state generation; loopback callback listener; authorize-URL construction; code→token exchange; expiry derivation (from expires_in or JWT exp); atomic mode-at-birth 0600 write of the credential file; amico pasqal connect verb wiring with browser-open + --no-open; tests for all of the above against a stubbed token endpoint.
Out: Connections panel webview + fork-server route wiring — separate slice, consumes this verb.
Out: project picker from the account API — separate slice; until then connect takes an explicit --project so the credential-file contract is unchanged.
Out: refresh-token silent renewal — depends on what Pasqal's client config grants; file shape reserves the field.
Out: device-code variant — blocked on Pasqal client config (probed 2026-07-21: grant disabled).
Assumptions / Open Qs
Blocked externally for prod use, not for merge: Pasqal's public client (PeZvo7…, shipped in their SDK) today rejects loopback redirects (403 Callback URL mismatch) and has device-code disabled — probed live 2026-07-21. The ask: mark/register a native OAuth client with loopback redirect URIs and any-port matching (or enable device-code). All endpoints/client-id are env-overridable, so the flow is fully testable now and lights up when Pasqal flips the config.
Assumes the panel/fork-route slice will hold its POST open while this verb runs and read the verb's single-line JSON result (the existing validator child contract style).
Acceptance Criteria
amico pasqal connect --project <id> opens the system browser to the authorize URL and, on callback with valid state, exchanges the code (with PKCE verifier) and writes the credential file readable by the existing launcher reader — round-trip proven in tests with a stubbed token endpoint and a synthetic callback request.
The credential file is created atomically with mode 0600 at birth (tmp file born 0600, then rename) — asserted by test, including the tmp-file mode.
The access token and refresh token never appear in argv, stdout JSON, error messages, or logs — the verb's JSON result carries identity/expiry/file-path only. (House AC3 discipline.)
Callback hardening: listener binds 127.0.0.1 only; mismatched or missing state → 400 and the attempt keeps waiting; error= callback (e.g. access_denied) → clean non-zero exit with an actionable message; configurable timeout closes the listener and exits distinctly.
Expiry is derived from expires_in when present, else the JWT exp claim (decoded unverified), else omitted — each path unit-tested; the stored expires_at satisfies assertPasqalFresh.
--no-open prints the authorize URL instead of spawning a browser (SSH/headless escape hatch); a failed browser spawn degrades to the same and keeps waiting.
Existing suite stays green; no changes to the launcher/reader contract or the connector scripts.
Key Decisions
Ephemeral port, bind-then-build: listener binds port 0 on 127.0.0.1 and the redirect URI is constructed from the actually-bound port — pre-registration squatting is impossible; a squatter racing a fixed port would surface as EADDRINUSE, never a silent hijack. This requires any-port loopback matching on Pasqal's client (part of the ask); a fixed-port fallback is a config value away if they refuse.
The verb, not the fork server, owns the listener — the fork server (fixed port 43117) may not exist in CLI/headless contexts, and its routes require the per-boot password that an Auth0 redirect cannot carry; the code is authenticated by state+PKCE instead.
Defaults for auth base / client id / audience are Pasqal's published prod values (public in their SDK source); env vars override all three (test stub + preprod).
Scope requested: openid profile email offline_access; identity (email) is read from the ID token for the panel's "Connected as" line and stored alongside the token; refresh_token, if granted, is stored for the future renewal slice. Extra keys are ignored by the existing reader — contract unchanged.
Failures are ConfigError/exit-64 class before spawn semantics apply; the callback-timeout and access-denied lanes get distinct messages pointing at the Connections panel.
Constraints & Invariants
Never bind beyond loopback; never accept a second in-flight attempt on one listener.
The password concept must not appear anywhere in this code path — this flow's entire premise is that Amico never sees it.
VS Code Remote translation (asExternalUri) may rewrite the callback port — another reason any-port matching is the ask; do not hardcode port assumptions anywhere but the bound-socket read.
Prior Art / Patterns
Launcher env-only secret carriage and token-free error discipline: the amico-pasqal launcher module and its tests.
Credential file location override idiom ($AMICO_PASQAL_FILE) and reader shape-checks: the launch module's reader (this slice writes what it reads).
Verb dispatch and {json, code} result shape: the existing amico pasqal verb.
Feasibility probes of 2026-07-21 against authenticate.pasqal.cloud: tenant supports authorization_code + device_code; the SDK's public client rejects both today (callback mismatch / grant disabled). Runtime-verified separately that token-provider injection works through the released SDK stack with no library patches.
Important
Problem
Pasqal Cloud credentials currently enter Amico through chat — the password rides the agent's command line (
ps-visible, transcript-persisted), exactly the exposure ADR 0002 calls out. The Connections panel design needs its auth flow implemented, and the resolved design is browser sign-in: the password never enters Amico at all.Approach
OAuth authorization-code + PKCE with a loopback callback (RFC 8252 — the
gh auth login/gcloudpattern), implemented as anamico pasqal connectCLI verb so the fork server's Connections routes reuse it via the established spawn-a-CLI-child pattern, and headless/opencode servesurfaces get the identical flow for free. Sign-in happens on Pasqal's own Auth0 page (password manager / SSO / MFA native); Amico receives a single-use code on a 127.0.0.1 listener, exchanges it in-process for a revocable JWT access token, and persists token-only per ADR 0001.Approaches Considered
vscode://URI-handler deep link back to the extension — rejected: extension-only surface, breaksopencode serve/CLI parity (ADR 0002's seam rationale).Scope
In: PKCE + state generation; loopback callback listener; authorize-URL construction; code→token exchange; expiry derivation (from
expires_inor JWTexp); atomic mode-at-birth 0600 write of the credential file;amico pasqal connectverb wiring with browser-open +--no-open; tests for all of the above against a stubbed token endpoint.Out: Connections panel webview + fork-server route wiring — separate slice, consumes this verb.
Out: project picker from the account API — separate slice; until then
connecttakes an explicit--projectso the credential-file contract is unchanged.Out: refresh-token silent renewal — depends on what Pasqal's client config grants; file shape reserves the field.
Out: device-code variant — blocked on Pasqal client config (probed 2026-07-21: grant disabled).
Assumptions / Open Qs
PeZvo7…, shipped in their SDK) today rejects loopback redirects (403 Callback URL mismatch) and has device-code disabled — probed live 2026-07-21. The ask: mark/register a native OAuth client with loopback redirect URIs and any-port matching (or enable device-code). All endpoints/client-id are env-overridable, so the flow is fully testable now and lights up when Pasqal flips the config.Acceptance Criteria
amico pasqal connect --project <id>opens the system browser to the authorize URL and, on callback with validstate, exchanges the code (with PKCE verifier) and writes the credential file readable by the existing launcher reader — round-trip proven in tests with a stubbed token endpoint and a synthetic callback request.state→ 400 and the attempt keeps waiting;error=callback (e.g. access_denied) → clean non-zero exit with an actionable message; configurable timeout closes the listener and exits distinctly.expires_inwhen present, else the JWTexpclaim (decoded unverified), else omitted — each path unit-tested; the storedexpires_atsatisfiesassertPasqalFresh.--no-openprints the authorize URL instead of spawning a browser (SSH/headless escape hatch); a failed browser spawn degrades to the same and keeps waiting.Key Decisions
state+PKCE instead.openid profile email offline_access; identity (email) is read from the ID token for the panel's "Connected as" line and stored alongside the token;refresh_token, if granted, is stored for the future renewal slice. Extra keys are ignored by the existing reader — contract unchanged.ConfigError/exit-64 class before spawn semantics apply; the callback-timeout and access-denied lanes get distinct messages pointing at the Connections panel.Constraints & Invariants
asExternalUri) may rewrite the callback port — another reason any-port matching is the ask; do not hardcode port assumptions anywhere but the bound-socket read.Prior Art / Patterns
amico-pasqallauncher module and its tests.$AMICO_PASQAL_FILE) and reader shape-checks: the launch module's reader (this slice writes what it reads).{json, code}result shape: the existingamico pasqalverb.Source
pasqal-auth-ux.htmlin harmoniqs_products).Notes
Feasibility probes of 2026-07-21 against
authenticate.pasqal.cloud: tenant supportsauthorization_code+device_code; the SDK's public client rejects both today (callback mismatch / grant disabled). Runtime-verified separately that token-provider injection works through the released SDK stack with no library patches.