# Copilot accounts

## Delivery status

This document records the approved requirements and implementation design for
issues #297 and #305. It is not a release receipt or a claim of current
installation/runtime state. The existing single-account behavior remains the
compatibility baseline; publication, installation and loaded runtime state are
separate evidence.

## Requirements

### Native OAuth refresh and dispatched requests (#385)

In 0.4.0, every matching credential notification aborts managed wires, including
normal native refresh by another same-account request. Upgrade to 0.4.1 for the
narrow plugin-owned refresh exception. This is synthetic regression evidence,
not attribution of a particular live error to refresh.

Only a genuine native refresh through the plugin's public credential-store bridge
may qualify: normalized account grant, model entitlements and official
OAuth-derived endpoint must remain unchanged. Its exact returned object is
consumed once inside the serialized `modifyRecord` callback. Exactly one
synchronous record notification and matching returned committed grant confirm
the write. No refresh-wide timing flag or unchanged-token heuristic is used.
Failed, duplicated, delayed, unknown or externally produced notifications remain
revocation; Core's separately owned canonical adapter has no provenance seam
through this bridge and remains fail-closed.

The exception retains only HTTP streams already dispatched through the native
adapter's public Fetch seam, including concurrent streams for the same frozen
account. Metadata, prepared leases, search/recovery proof and retry snapshots are
still invalidated immediately. A new step/request must resolve fresh native auth
and validate current account metadata. An old prepared request cannot dispatch
after renewal; an already-dispatched request cannot use its old lease to make a
second physical HTTP attempt after renewal. Core retains retry/transaction
ownership; this plugin adds no resend or automatic retry. Account default changes
do not redirect frozen requests; each account's record remains isolated.

Account/endpoint/permission replacement, record removal, read/commit failure,
caller cancellation and disposal still revoke. Supplier expired-token rejection
and network failures can still terminate the original request; preserving a local
stream does not prove supplier acceptance. Identity/quota presentation caches
continue to invalidate on every notification, not reuse old credentials.

### Selector switching and account-bound turns (#344)

A global switch changes only the selector. Explicit record-bound request leases
may be acquired during its preflight and CAS so already admitted turns can
prepare and dispatch their next native step with their frozen account. A new
turn still captures its account once at admission; it does not adopt a later
selector. Unbound/global leases remain blocked during switching, and all
requests remain blocked during credential authorization/removal/sign-out.
Pinned records cannot be reauthorized or removed until their leases end.
Cancellation, route eligibility, credential validity and account-model checks
remain independent. This is not automatic retry or permission to redirect an
existing turn. Deferred native fixtures exercise all three supported protocols
while the selector operation is still pending, not just after it settles.

### Global switch responsiveness (#342)

Models first reads metadata-only `get()` and separately hydrates display names
with nonforcing `ensureIdentity()`. Slow inactive-name queries do not hold the
Switch control. Unknown names remain unknown; route/activity/membership evidence
still gates controls and the Host independently verifies any requested switch.
An identity hydration result captured before a mutation or actual Remote/
authorization/credential lifecycle replacement cannot overwrite newer state.
Parent callback identity changes alone do not recreate the account lifetime.

The Host overlaps fresh identity and available-model preflight under one
60-second operation signal. It never replaces fresh verification with the
ten-minute display cache. Both must finish successfully before selector CAS;
identity key, membership, route/activity, canonical authorization and revision
checks remain, including post-commit confirmation. A failed branch aborts the
other and drains both before releasing the mutation fence, without automatic
retry or another account fallback. The strict save result updates the account
card; parent status refresh remains separate. This reduces a serial preflight
critical path to overlapping work, not zero network latency or proven live
timing. Synthetic deferred tests establish ordering, not production speed.

- Keep multiple independently authorized GitHub Copilot accounts in one DSH
  profile. Models retains the global default. Sessions without an override
  inherit that default at each new turn; explicit Session choices take priority.
- Models manages reauthorization/removal and the global default; Credits supports
  add-only authorization without selection. Its compact no-search dropdown
  applies to this Session's subsequent turns. No standalone
  composer selector, one-turn override or DSH application identity replacement.
- Persist OAuth grants through the existing DSH credentials service. Use
  official pi-ai login, refresh and transport, not copied credentials, a token
  backup/refill scheme, a separate credential store, a second wire or Core edits.
- Preserve the existing `llm-pi-ai/github-copilot` record without moving or
  copying it. It is the compatibility account when no active account is stored.
  Newly added accounts are directly authorized into plugin-owned records.
- Changing the account must not change Session histories, selected models,
  future global defaults, model exclusions or billing/Usage records. A selected
  model unavailable to the new account fails explicitly, without substitution.
- Freeze an admitted turn's account before Auto assessment. Later Session/global
  choices do not redirect its steps, native retries or hosted search. Block
  reauthorization/removal of its pinned record until native turn end/disposal.
  Never cancel a running answer automatically. Explain that encrypted historical replay can be
  account-bound; do not silently remove replay or replay a message.
- No automatic fallback to another account on quota exhaustion, authorization
  failure, missing records or invalid settings.

## Public API evidence

Official DSH `0.2.0-rc.2`, commit
`639ed015397290b3745d163aafe02ffee4aa3f84`, exposes
`PiAiAdapterOptions.auth.credentials`. Its adapter source was compared
byte-for-byte with that commit. DSH `authorization.registerFlow()` registers a
flow per credential key; `credentials.modifyRecord()` serializes mutations.

Published pi-ai `0.87.1` accepts a public `CredentialStore` in `createModels()`.
`Models.login()` persists directly through that store. `Models.getAuth()`
refreshes OAuth under its serialized mutation; each account can supply an
independent store binding without changing provider IDs or model IDs.

An isolated experiment with unchanged pi-ai and synthetic in-memory stores
verified independent native login, no refresh for valid tokens, parallel
account-local refresh, captured-store stability after selection changes,
preservation after refresh failure, and independent logout. All HTTP was
mocked. This is SDK composition evidence, not live OAuth, DSH durable storage,
native gateway or model-call proof.

GitHub's authenticated `GET /user` returns the identity corresponding to the
GitHub authorization token. The current native OAuth requests `read:user`.
Its `refresh` field holds that GitHub token; the short-lived `access` field
holds a different Copilot request token. Never exchange their purposes.

The Copilot SDK `account.getQuota` contract at commit
`ef04633cc84e4ba8e79888a39259ca276f5de732` returns quota snapshots and accepts
an account selection identifier or GitHub token. It is an experimental
SDK/runtime RPC, not a pi-ai API or proof of cross-application AI credits.
This feature does not introduce another CLI runtime to obtain quota.

## Ownership extension

The user explicitly approved this limited extension of the previous
single-record policy:

- Official canonical credentials remain under their existing owner.
- The plugin may own additional `github-copilot/account-<opaque-id>` records,
  obtained by official OAuth writing directly into the target record.
- All records use the existing DSH credentials service and documented grant
  normalization. No credentials enter Settings, Client, Remotes, logs or docs.
- Official pi-ai remains the OAuth and refresh implementation owner. The plugin
  owns only account addressing, selection, lifecycle and safe presentation.
- This does not authorize Core changes, credential cloning, ambient credential
  discovery, copied upstream catalogs or a second general transport adapter.

## Account binding and persistence

Settings contain an opaque global active account identifier, written with a
path-level compare-and-swap in the existing `github-copilot` namespace. An
absent selector means the canonical compatibility account. An invalid selector
or missing selected record is an error, not permission to use canonical.
There is no persisted membership list: native `credentials.listRecords()`
enumerates record keys and kinds, while public identity stays in bounded
Host-memory cache. No usernames, grants or auxiliary account list are stored
in Settings. Hidden volatile `sessionAccounts` additionally persists bounded
`{sessionId, accountId}` overrides, changed through a separate path-level CAS.
Selecting the same account as the default is still an explicit override.
Choosing **Follow global default** removes only this Session's row.

A request captures an immutable account identifier, record key and selection
ownership. Its `CredentialStore.read/modify/delete` methods all refer to that
record. A mutable active-account lookup inside each store method is forbidden:
an A refresh must never commit into B after a switch.

Account identity uses GitHub host plus stable numeric user ID for duplicate
detection, not usernames or token strings. Identity reads are bounded,
Host-only, redirect-refusing and cancellation-aware. Normalize only required
public identity fields, discard unrelated `/user` fields, and reject results
whose credential/selection evidence changed during the read.
Host normalization and strict Remote decoding share the same bounded login
pattern. It accepts ordinary names and GitHub.com Enterprise Managed User names
with underscore-separated shortcodes, including setup admin names. This is not
support for a custom enterprise OAuth host; `enterpriseUrl` remains unsupported.
See [GitHub's official username rules](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/iam-configuration-reference/username-considerations-for-external-authentication).

Token refresh remains lazy. Inactive accounts need not hold fresh Copilot
tokens. Refresh uses native `Models.getAuth()` and the corresponding DSH record
mutation. Failure preserves the account's stored grant and does not affect
other records. Reauthorization targets the same account slot and must detect
unexpected identity replacement before activating it.

## Add and switch transactions

Adding an account runs native OAuth in an independent slot. It does not
overwrite or change the active account. Identity and model metadata validation
must complete before the new slot can be selected. Partial authorization is
reported truthfully and can be retried or explicitly removed.

The Models card keeps the GitHub verification link, selectable manual URL,
one-time authorization code, copy action and cancellation together while
device authorization is pending. A confirmed OAuth completion is followed by
a separately reported identity-and-model verification phase; neither phase
changes the global default. The Host's operation state, not a Client guess,
proves when that verification phase begins. Normal pending authorization is
progress, not a terminal busy error; technical diagnostics remain available in
a collapsed disclosure. Unknown blockers are not replaced with a speculative
list. **Refresh account information** remains an identity refresh only and
does not complete or cancel authorization.

Switching has a preparation phase and a commit phase:

Route eligibility reads official rc.2 SettingsForms through redacted
`describe()` values and revision-bearing namespaces; it does not require the
retired `get()` method. Missing or malformed configuration still means
incomplete evidence, not an absent native route. The legacy reader remains
available for retained compatibility fixtures. Account persistence acceptance
uses real SettingsForms and the production route diagnostic, without an
eligibility override.

1. Capture the selector revision and verify managed-only route eligibility.
2. Validate target identity, refresh authorization as needed and check account
   model metadata without publishing it as the active account.
3. Hold an admission fence during commit. Existing account-pinned managed
   turns may continue; unbound legacy preparation remains conservatively fenced.
4. CAS the selector, read back committed settings and confirm target evidence.
5. Update the default directory and UI without revoking another account's
   running turn/proofs or writing model/history/default selection. Credential
   replacement revokes only the matching account runtime and search/quota proof.

`COPILOT_ACCOUNTS_BUSY` is transient: each account status read reevaluates
current managed Agent activity, operation leases and authorization. A rejected
switch must not remain busy after that work settles. Reading status does not
refresh credentials or retry the rejected switch. In Models, use **Refresh
account information** after work ends to update the mounted view. With only
one saved account, first use **Add GitHub account**; a **Switch** control is
shown only for another saved account. No running work is cancelled for this.

Failed preparation leaves the old selector unchanged. A committed selector
followed by failed readback is reported as uncertain, never successful and
never automatically rolled back. Cross-process/external mutations are guarded
by revisions and evidence checks; an in-process fence is not a cross-namespace
or cross-process transaction guarantee.

Operations include model preparation, Auto assessment, model discovery,
native auth refresh, managed answer streams and hosted search. Release leases
on completion, failure, cancellation and disposal. Switching never terminates
native retries, rewrites native retry policy or manufactures Usage.
An abandoned signal-free prepared call has no public completion/cancellation
evidence; it conservatively retains its account mutation barrier until disposal.
Changing the global default can still proceed without redirecting that call.
A guessed idle timeout must not permit reauthorization/removal of its account.

## Route compatibility

Core's canonical route still reads its canonical record. Do not intercept
Core's store, mutate its prototypes or mirror selected grants into its record.
The first multi-account delivery therefore requires a managed-only profile.
Existing canonical configuration, running/selected legacy routes, or
incomplete route evidence blocks switching with a named diagnostic.

Any migration uses the existing separately approved, explicit configuration
workflow. Enabling accounts does not remove native profiles, rewrite stored
Sessions or silently migrate their route IDs.

Sign-out deletes only the selected record. Inactive records remain intact.
For a noncanonical selection, the selector remains unchanged after sign-out:
the missing selected slot fails explicitly, never falls back to canonical.
In a managed-only profile with complete route/Session evidence and no admitted
activity, Models still permits an explicit CAS switch to a stored valid account
or Add into a new inactive slot. The missing-slot view remains an error even
when `switchable` is true for that recovery. Existing Sign in never recreates
the missing slot with an unverified replacement identity; use Add then Switch.

## Presentation and quota

Models shows current identity and available accounts, Add, Switch and explicit
authorization recovery. Switch confirmation describes profile-wide impact and
history/replay risks. Activity and route blockers remain visible.

![Models account controls at a mobile viewport](./images/copilot-accounts.png)

![Read-only identity in Credits details](./images/copilot-accounts-credits.png)

These images render the actual candidate components with synthetic accounts
and quota in an isolated browser. They are not real authorization, supplier
availability or loaded Desktop evidence.

The separate `githubCopilotAccounts` namespace exposes `get`, `ensureIdentity`,
`refreshIdentity`, `add`, `cancel`, `switchAccount`, `reauthorize` and
`removeAccount`. Existing Remote descriptors are retained. The removal method
cannot be named `remove`: official rc.2 reserves that inherited service name
and rejects the entire Client contribution.

Credits shows the Session's next-turn account and quota under the existing title.
Its **Switch** dropdown marks the current choice, scrolls long lists internally
and offers **Follow global default** plus add-only authorization at the bottom.
Reauthorization, removal and global
selection remain in Models. Changing
the preference while a turn runs leaves that turn's old account pinned; the
popover identifies it separately. Other Sessions are unchanged. Unknown identity
is unavailable, not a stale username presented as current.

One Models **Manage** groups account management, the new-Session continuation
default and shared exact-ID model preferences. The existing Session policy is
under the composer Credits/usage popover → **Visible-history continuation** →
**Session policy**; it appears only for an open Session whose effective model
is the managed account-discovered Copilot route. Choose **On** after reviewing
the loss disclosure. This is distinct from the Models new-Session default and
applies to subsequent turns and managed native compaction summaries, including
on the same account. Active turns keep their captured policy; enabling does not
send or retry a message. See the [synthetic composer capture](./images/copilot-session-continuation.png)
and [continuation contract](./session-continuation.md). When off, an account change pauses for explicit
persistent-enable/keep-off/cancel consent. See the [approved experience and mock](./account-management-experience.md).

Completed managed turns have a compact **Account** disclosure alongside native
Usage/selection actions through the public assistant-actions slot. This is
request-account evidence, not billing attribution or a subagent account rollup.
It freezes a verified cached identity at admission. Missing identity starts one
nonforcing, bounded lookup for the exact admitted account without delaying model
delivery. A successful lookup may fill missing identity only while that same
turn/signal remains active and uncancelled, retaining the first verified name.
Account execution evidence is still created only on the first real ordinary
native chunk, not discovery, preparation or auxiliary Auto assessment. Late
lookups never backfill completed turns. Host memory retains at
most 64 Sessions and 128 turns per Session. Restart, cold history, eviction or a
request with no native delivery is unknown. Failed reads are distinct from
unknown evidence; Retry rereads the same turn without inference. Official rc.2
has no ignorable account-history event seam: no new durable event is emitted.
Only a login captured for that turn is shown; otherwise identity is explicitly
unavailable, never inferred from current settings or today's account selector.
Credits selector labels unknown original/saved authorizations without exposing
opaque record identifiers; these presentation labels do not fetch identity or
change account selection.

Identity and quota have separate error states but must share account evidence.
Models account mutations invalidate mounted Credits presentation before the
request and reject late responses. A data-free plugin-owned notification causes
a fresh read after settlement; it does not cancel native work or broadcast
identity, grants or quota between surfaces. Failed reads never restore the
previous username as current.
Missing quota is not zero; pooled, stale, request-based and credit-based
snapshots retain their distinct meanings. Do not claim cross-app aggregation
or relabel request counts as AI credits.

Visible Models and Credits surfaces ensure identity on mount, visibility return
and their sixty-second display cadence. `get` and `viewForAccount` remain
metadata-only. Nonforcing ensure reuses a fresh ten-minute identity, joins
same-account in-flight work and observes a thirty-second failure cooldown;
explicit **Refresh account information** bypasses cooldown but joins active
work. There is no render-driven request loop or background polling after
unmount. Credential changes revoke identity and pending work; cancelled or
unverified identity is never shown as current. Verified display names remain in
the bounded Host cache beyond the ten-minute revalidation interval; freshness
expiry alone never replaces an inactive account name with unknown. Names are
display metadata, not authorization/model-access proof, and are not persisted
across Host restarts. Models and Session Credits ensure missing configured
account names through the same per-account lookup without activating those
accounts. Membership limits bound the parallel lookups and each retains its
timeout/single flight/cooldown. Credential invalidation and failed verification
still clear names. Session ensure retains explicit
Agent lookup and rechecks account selection before returning.

Quota settles independently of identity renewal only after account-coherent
metadata checks. Identity failures cannot turn account quota into zero or
discard an otherwise valid same-account reading. Sanitized diagnostics separate
timeout, certificate, network, auth rejection, rate limiting and other HTTP
failures; no raw response/error text or credentials enter the Remote. These
diagnostics do not establish the cause of earlier screenshots or prove live
supplier health. Account switching, duplicate checks and reauthorization still
require fresh authenticated identity validation, not the presentation cache.

## Acceptance gates

- Native public authorization and credentials services: independent writes,
  cancellation, restart persistence, per-record serialization and strict grant
  JSON. No changes to the official fixture implementation.
- Both public gateways and strict Client codecs: no credential fields, stable
  descriptor identities, bounded arguments and visible errors.
- Exact published native adapter: correct account auth on all supported
  protocols; account-local refresh and failure; no ambient fallback.
- Races: A refresh versus B selection, admission versus switch, external record
  replacement, settings conflicts, disposal and late identity/quota responses.
- Behavior: absent-selector compatibility, missing-selector fail-closed,
  duplicate identity, same-account renewal, canonical route blocker, selected
  model missing, revoked permissions and account-bound replay.
- UI: Models global-default management, Session switching only inside Credits,
  global inheritance/explicit same-value override, frozen running turns,
  historical Account/unknown/read-error distinctions, dark/light/mobile,
  keyboard interaction, loading/error states and no overlapping controllers.
- No native Usage filtering, history edits, default selection changes or
  automatic model/account substitution.
- Full current rc.2 Windows/Linux CI, package and tarball gates, reviewed merge,
  immutable GitHub Release and same-byte npm publication before claiming
  delivery. Installation and restart need separate authorization.

## Current evidence and limits

Focused native adapter and gateway tests use unchanged rc.2 public APIs and
synthetic credentials/HTTP. They exercise Responses, Chat Completions and
Anthropic account dispatch, immutable refresh bindings, strict gateway
reachability, target CAS, admission and same-identity renewal.

The temporary-profile persistence fixture uses native `LocalCredentialProvider`,
profile boot/Loader, ConfigEditor, SettingsForms CAS, authorization and SDK.
Independent grants and the selector survive disposal/reopen; stale CAS does
not replace the config fiber; account A refresh never writes B. It injects
model preflight, and mocks identity/provider
HTTP. It is not full production lifecycle or live OAuth qualification.

The local frozen install and full gate are blocked by HTTP 404s for unchanged
official rc.2 dependencies from the configured enterprise mirror. No package
versions, registry policy or Core artifacts were substituted. The complete
required Windows/Linux CI matrix remains the release gate; focused tests,
synthetic UI and persistence do not replace it or prove live model success.
