feat(runtime-host): sign in to GitHub Copilot with a device grant - #3390
feat(runtime-host): sign in to GitHub Copilot with a device grant#3390hgaol wants to merge 34 commits into
Conversation
c7d8924 to
4f133f1
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for filling the sign-in gap described in #3388. The problem is valid, and the provider-specific RFC 8628 parsing is carefully bounded.
The remaining implementation concern is ownership: the device flow currently runs beside the existing Host OAuth authority, duplicating the attempt lifecycle and producing the race described inline. The simpler final state appears to be extending the existing Host OAuth provider slice for GitHub Copilot and keeping Desktop responsible only for importing credentials that genuinely depend on the local gh installation.
I also left two independent inline notes about token lifetime and the OAuth app identity.
AI-assisted review disclosure: Codex performed exact-head analysis, with independent reviewer passes focused on lifecycle, concurrency, security, compatibility, and OAuth protocol behavior. The findings were revalidated against head 4f133f11b4442ed572379da7040965e9a7699ca2.
| // subscription has no other client to present. The provider already ships | ||
| // the matching editor headers (GITHUB_COPILOT_COMPAT_HEADERS), so this | ||
| // changes how the credential is obtained, not who Maka claims to be. | ||
| clientId: 'Iv1.b507a08c87ecfe98', |
There was a problem hiding this comment.
[P1] Establish the OAuth app identity before shipping
Thanks for documenting why this client ID is needed for Copilot access. Could we also link the public authorization or compatibility basis that permits Maka to use the GitHub Copilot editor/CLI OAuth identity?
GitHub’s consent screen identifies that application while Maka receives and stores the resulting credential. If GitHub has not explicitly approved third-party reuse, this should use an app identity authorized for Maka. Either way, the source, consent identity, and authorization basis should be recorded before this becomes a public sign-in path.
There was a problem hiding this comment.
Agreed on the provenance question. I recorded the client identity, consent model, and the open authorization basis in docs/github-copilot-oauth-identity.md, and called the same out next to the client ID in packages/runtime/src/oauth-provider-contracts.ts.
The interactive sign-in ships on, with the same style of kill switch as Codex: MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=0 refuses enrollment at the Host without a release. Local gh / fine-grained PAT import stays available beside it as the fallback that does not reuse this identity.
Open decision (reviewer call): either a published GitHub authorization/compatibility basis for reusing this editor OAuth identity (linked from that doc), or a Maka-owned app identity that replaces Iv1.b507a08c87ecfe98. I did not invent a basis that does not exist.
There was a problem hiding this comment.
English
The current exact head 0685beb21aada31cfedde4ab92a18a8c10ebb03a still leaves this [P1] open. Making the route opt-in does not establish permission to use the borrowed OAuth identity: an operator can enable the flow, but cannot authorize Maka to act as GitHub's Copilot editor application or make the consent screen identify the software that will receive and store the token.
This is executable, not documentation-only. Setting MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 admits the Host flow through this contract, requests the device code with the borrowed clientId, polls for the user token, verifies entitlement, and commits the credential to Maka's vault. The exact-head identity document also says no published authorization or compatibility basis has been established.
Please keep local credential import as the supported path and remove the executable borrowed-identity enrollment until Maka has either its own authorized application identity or a published GitHub authorization/compatibility basis. Default-off is a useful rollout guard, but it is not a consent or authorization guard.
中文
当前 exact head 0685beb21aada31cfedde4ab92a18a8c10ebb03a 仍未关闭这条 [P1]。把入口改成显式启用,并不能建立复用该 OAuth 身份的授权依据:操作者可以开启流程,但不能授权 Maka 冒用 GitHub Copilot 编辑器应用的身份,也不能让同意页准确说明最终接收并保存 token 的软件。
这条路径可以真实执行,并非只有文档。设置 MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 后,Host 会通过这里的契约放行流程,使用借来的 clientId 申请设备码、轮询用户 token、校验订阅资格,并把凭证写入 Maka 的 vault。当前 head 的身份说明文档也明确记录:尚未建立公开的授权或兼容性依据。
建议继续保留本地凭证导入作为受支持路径,并在 Maka 获得自有、已授权的应用身份,或 GitHub 发布明确的授权/兼容性依据之前,移除这条可执行的借用身份登录路径。默认关闭是有用的发布守卫,但不是同意或授权守卫。
There was a problem hiding this comment.
English
Revalidated on exact head 0b9ebaf938178cf640309d44064021a8eaa1500c: this [P1] is still open. The new head is a merge of the previously reviewed head with upstream 5525be38f; the OAuth contract, identity document, and Host device-enrollment path are byte-identical to 0685beb21aada31cfedde4ab92a18a8c10ebb03a. Setting MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 therefore still executes the borrowed-identity grant and commits the resulting token without an established authorization basis.
The merge correctly moved this PR's compatibility epoch past upstream's then-current epoch, but it does not change the consent/identity boundary. The requested resolution remains: keep local credential import, and do not ship executable borrowed-identity enrollment until Maka has its own authorized app identity or a published GitHub authorization/compatibility basis.
中文
已在 exact head 0b9ebaf938178cf640309d44064021a8eaa1500c 上重新验证:这条 [P1] 仍然成立。新 head 只是把上一轮已审 head 与 upstream 5525be38f 合并;OAuth 契约、身份说明文档和 Host 设备登录路径与 0685beb21aada31cfedde4ab92a18a8c10ebb03a 字节一致。因此设置 MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 后,仍会执行借用身份的 grant 并在没有既定授权依据的情况下提交所得 token。
这次 merge 正确地把本 PR 的 compatibility epoch 提升到当时 upstream epoch 之后,但没有改变 consent/identity 边界。修复方向不变:保留本地凭证导入;在 Maka 获得自有且已授权的应用身份,或 GitHub 发布明确的授权/兼容性依据之前,不要交付可执行的借用身份登录路径。
There was a problem hiding this comment.
Keeping the device sign-in, and proposing this be settled across providers rather than per-branch.
In current main: openai-codex ships OpenAI's CLI client id enabled by default (its flag is a kill switch), xai-oauth ships grok-cli's unconditionally with no flag, and GITHUB_COPILOT_COMPAT_HEADERS already presents Maka as VS Code's Copilot Chat on every Copilot call — including the local-import path this thread asks me to keep. The standard here isn't one the tree currently meets, and this flow is the strictest of the three: off unless an operator opts in.
Your distinction holds and I won't pretend otherwise — the other two never drive a consent screen naming a different application. What I'm disputing is settling that one branch at a time while two default-on borrowed identities sit in main unexamined.
Proposal: I file one issue covering all three identities — provenance, consent identity, authorization basis, what would settle each. This branch keeps the strictest posture meanwhile. If borrowed identities go, they should go consistently.
There was a problem hiding this comment.
[P1] Revalidated on exact head 02ee0bf874912102984991b31ff64c588649506e: this remains open.
The OAuth contract and identity record are byte-identical to the last reviewed head. Enabling MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 still makes Maka request a device grant with GitHub's Copilot editor application identity, show a consent page naming that other application, receive the resulting token, and store it in Maka's vault. The repository still states that no GitHub authorization or compatibility basis for this reuse has been established.
Handling other borrowed identities in a separate issue would be useful, but it does not close this path. As the author notes above, those other paths do not present a consent screen that names a different application; this one does. The repair remains to keep local credential import, and remove the executable borrowed-identity flow until Maka has its own authorized application identity or a published GitHub authorization/compatibility basis.
Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.
简体中文
本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。
|
PR need a rebase before reviewing and approve~ |
GitHub Copilot is the only registered oauth_token provider without a login flow. Desktop's connectExistingLogin only scavenges COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN, or `gh auth token`, rejects classic PATs, and needs a credential carrying Copilot Requests — a permission plain `gh auth login` does not grant. Users who own a subscription still have no way to sign in. Add the RFC 8628 device authorization the provider was missing, alongside the existing Codex and xAI enrollments. The grant yields the same gho_/ghu_ account token createGitHubCopilotAccountTokens already accepts, so only credential acquisition changes. GitHub reports authorization_pending and slow_down as HTTP 200 with an error body rather than a 4xx, so the error code is read before the success shape. The verification URL is pinned to github.com because the presentation layer opens it in the user's browser, and only read:user is requested, so the grant cannot reach a user's code. The provider already sends the vscode Copilot Chat editor headers (GITHUB_COPILOT_COMPAT_HEADERS), so the client id presented here does not change who Maka claims to be. Fixes apache#3388 Generated-by: GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire the device enrollment into Settings so a user with a Copilot subscription can sign in, instead of first minting a fine-grained PAT with Copilot Requests or having a `gh` login that happens to carry it. `beginDeviceLogin` asks GitHub for a code and `completeDeviceLogin` waits for the user to authorize it; both ride the existing direct-account login flow, so the pending guard, snapshot refresh, and unmount safety stay in one place. The pending grant lives in the main process, so closing Settings cannot strand a poll holding a credential, and the secret still reaches only the Host vault. Importing an existing `gh` credential remains available as a secondary action for machines that are already authenticated, and both paths now adopt an account through one place that refuses to store a credential before the account proves it returns usable models. The verification code is shown as text rather than opening a browser: the renderer has no general openExternal bridge by design, and the device URL is pinned to github.com by the enrollment. Fixes apache#3388 Generated-by: GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Route the GitHub Copilot device grant through `HostOAuthCoordinator` instead of a second login-session authority in Desktop. Assigning `deviceLogin` only after the begin request resolved let a reopened Settings pane or a second IPC client overlap attempts, so an older completion could replace the credential a newer login had just committed. `github-copilot` joins `OAUTH_LOGIN_PROVIDERS` and the interactive login providers, and the coordinator grows a provider slice beside xAI and Codex. It therefore inherits one serialized start, supersede and cancellation boundaries, Host residency across polling, the configured network transport, and an atomic credential commit. Desktop keeps only the local `gh` credential import; account state, refresh, and sign-out ride the shared channels the coordinator's adapter already registers. Older peers cannot start a login for the new provider, so the compatibility epoch moves to 32. Preserve the lifetime GitHub returns. Every account token was recorded with `expires_at = Number.MAX_SAFE_INTEGER` while the refresh path returned it unchanged, so an expiring token would stop working at expiry and require another interactive login. The enrollment now carries `expires_in` and `refresh_token` through, refresh performs the refresh grant for an expiring record, and an expiring response with no refresh token is rejected rather than stored. A token with no declared lifetime keeps the sentinel and still refreshes without provider I/O. Record the OAuth app identity. The client id is GitHub's Copilot editor app: GitHub's consent screen names that application while Maka receives and stores the credential, and no published authorization for third-party reuse exists. Provenance, consent identity, and the open authorization question are written down in `docs/github-copilot-oauth-identity.md`, and the device flow is now opt-in behind `MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1` so it is not a public sign-in path before that basis is established. Importing a credential the user already holds stays the shipped route. Generated-by: Claude Code
The device flow was gated opt-in, which left Settings with no sign-in button at all — only the local credential import. Recording the app identity is what the review asked for; hiding the feature was not. Enrollment now ships on with a kill switch, the same shape Codex already uses: `MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=0` refuses the login at the Host. The panel stops probing that flag and always renders the button, because the Host owns the decision and answers a disabled start with `experimental_disabled`; a second copy of the gate in the renderer could only disagree with it. The consent identity question in `docs/github-copilot-oauth-identity.md` is unchanged and still open. Generated-by: Claude Code
a3dcc93 to
16f6520
Compare
Rebased |
…-device-flow-login # Conflicts: # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for moving GitHub Copilot enrollment onto HostOAuthCoordinator. That resolves the original lifecycle and stale-completion problem, and the token lifetime/refresh work is now on the right authority.
I do not think another round of isolated patches would be fair or productive here. Could we converge on one final state instead?
[P1] Shipping boundary: the current provenance document explicitly says the authorization basis for reusing the Copilot editor OAuth identity is not established, while the feature is enabled by default. Until GitHub provides a public compatibility basis or a Maka-authorized app identity, this path should be explicit opt-in/default-off. The kill switch can remain for operations, but it cannot serve as the authorization basis.
[P2] Account adoption: device authorization should not commit a credential until the GitHub provider slice has verified that the account exposes at least one usable Copilot model. That validation belongs before the coordinator’s atomic commit, using the same Host transport; the Desktop post-login synchronization should not be responsible for deciding whether authentication succeeded.
[P2] Refresh completion: GitHub Copilot should join the existing single-401 refresh/replay seam. An access token can be rejected before its local expires_at; in that case the Host should force-refresh once, CAS-persist the new generation, replace the Authorization header, and replay the request once.
I would also like this revision to remove the Desktop lifecycle that the Host path has replaced, rather than adding the fixes on top:
- fold
GitHubCopilotSubscriptionServiceinto a stateless “import existing local credential” adapter; - remove the unused
directmode fromuseOAuthLoginFlow; - route connection-detail re-login through the shared Host OAuth service;
- keep local
ghimport only as a secondary provider-catalog action; - remove the unused
isExperimentalEnabledpreload/IPC query surface.
The provider-specific device parsing and its security/error tests should stay. The goal is not to make OAuth artificially tiny; it is to leave one lifecycle authority and remove the roughly 280–330 lines whose responsibilities have moved to the Host.
A clean final shape would be: Runtime owns the GitHub device codec, Host owns attempt/entitlement/credential/refresh/401 recovery, and Desktop owns only generic presentation plus local credential discovery. Once that state is in one exact head with epoch 35 and focused entitlement/401 regressions, we can review the final result as a whole instead of asking for another sequence of patches.
AI-assisted review disclosure: OpenAI Codex coordinated independent exact-head reviews of the OAuth lifecycle, consent/provenance boundary, refresh behavior, and simplification opportunities. I reviewed and approved this final-state recommendation before posting.
Leave one lifecycle authority for Copilot accounts. The Host owns the attempt, the entitlement check, the credential, refresh, and 401 recovery; Desktop keeps only generic presentation and discovery of a credential this machine already holds. The device sign-in ships off. Maka has no published authorization for the editor OAuth identity the grant presents, so an install opts in explicitly with `MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1`. This reverses the default chosen in "restore the GitHub Copilot sign-in in Settings": the flag records an operator accepting the consent mismatch for their own install, and cannot stand in for a basis that does not exist. A GitHub account is not a Copilot subscription. The provider slice now asks the Copilot API what the authorized account reaches, on the Host transport, before the coordinator's atomic commit; an account with no usable model fails the attempt as `provider_rejected` instead of committing a credential that breaks on first use. Copilot joins the single-401 refresh seam: force-refresh once, CAS-persist the new generation, replace the Authorization header, replay once. A record GitHub declared no lifetime for resolves to the token just rejected, so that replay is skipped rather than spent. Removed with their responsibilities: the Desktop subscription service, the `direct` mode in the login controller, the Copilot-specific re-login notice, and the unused `isExperimentalEnabled` query surface. Generated-by: Claude Code
An account Connection is created before anyone can ask the account what it has — the OAuth login path holds no credential at that point — so its enabled ids start as the provider's curated fallback list. Discovery then ran without ever reconciling them, leaving every guessed id selectable, testable, and sendable on an account that may never have exposed it. GitHub Copilot surfaced this: a connection test picks the first enabled id, the Copilot models endpoint does not list it, and the test fails on a model the user never chose while chat works on one they did. The local credential import was unaffected because it passes discovered ids at creation, so only the shared OAuth path carried the gap. Once a fetched inventory exists it is the authority: ids it does not list are dropped. Ids are never added, so a narrower selection the user made survives, and the discovered list is adopted whole only when nothing they kept remains. Generated-by: Claude Code
The notice opened the provider's device page and showed nothing else, but that page accepts nothing until the user types the one-time code the Host received. Re-login was therefore a button that led to a dead end. The code now appears beside the notice's own copy, for the flows whose device page asks for it. xAI is excluded because its verification URL carries the code itself, which is why the catalog panel has always hidden it there too. Generated-by: Claude Code
…b-copilot-device-flow-login # Conflicts: # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts
An account Connection is created before anyone can ask the account what it has — the OAuth login path holds no credential at that point — so its enabled ids start as the provider's curated fallback list, in the order this build ships them. Taking the first of those as the account's initial default names a model the account may never serve, which is how a GitHub Copilot connection test came to fail on a model the user never chose while chat worked on one they did. The live response now picks the order that first default is chosen from. It does not pick the set: an id the response omitted stays enabled, because a `/models` answer that cannot see a model is not evidence the account cannot run it — the rule `authorizeConnectionModel` states for every other reader of an inventory. This replaces the pruning added in "adopt the account's own model inventory after sign-in", which reached the same connection test by treating the live list as an allowlist over ids only the user writes. Generated-by: Claude Code
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for taking this through to a much cleaner final state. I re-reviewed exact head b5da959.
The original blocking concerns are resolved: interactive enrollment now runs through HostOAuthCoordinator, Copilot entitlement is verified before the atomic credential commit, rejected access tokens join the shared single-401 refresh/replay path, and the borrowed OAuth identity is default-off behind an explicit per-install opt-in. The replaced Desktop lifecycle, direct login mode, special re-login path, and unused experimental query have also been removed.
I left two non-blocking P2 comments. They are two remaining expressions of the same ownership invariant: the selected Runtime Host should be the sole authority for enrollment availability, entitlement, and credential adoption, while Desktop should only present the flow and discover credentials that exist locally. Neither warrants another broad redesign or a Request Changes review; the clean final direction is described inline.
The provenance document correctly states that authorization to reuse the editor OAuth identity has not been established. This approval accepts the dormant, explicit-opt-in shipping boundary for this change; it does not assert or create authorization from GitHub.
AI-assisted review disclosure: OpenAI Codex coordinated independent exact-head reviews of OAuth lifecycle and concurrency, identity and shipping boundaries, refresh behavior, remote-Host compatibility, and simplification. I verified the final diff, relevant source paths, prior findings, current head, CI, review threads, and the approval recommendation before posting.
| * Desktop owns exactly one thing for GitHub Copilot: importing a credential | ||
| * that already exists on this machine (`gh` / a compatible PAT). Interactive | ||
| * enrollment is not here — the device grant runs through the Host's OAuth | ||
| * coordinator like every other account login, so there is one authority that |
There was a problem hiding this comment.
[P2] Keep local credential import discovery-only
Reachability: ② reasonable concurrent or multi-client path. The local adapter currently performs entitlement discovery in Desktop, then this handler reads the Host's current credential revision and commits the imported secret. If import A is waiting on model discovery while an interactive login B commits through HostOAuthCoordinator, A can resume, adopt B's latest revision as its expected value, and overwrite the newer login. A remote Host also validates the import through the Desktop network environment rather than the Host transport.
The clean final state is for Desktop to return only the locally discovered raw credential and for a Host adoption operation to own entitlement, generation/supersede, and the atomic credential commit alongside interactive login. That closes the race and preserves one network and credential authority without adding another Desktop guard.
There was a problem hiding this comment.
[P2] Reproduced on exact head 02ee0bf874912102984991b31ff64c588649506e: the local import can still replace a newer interactive login.
I paused local import A during its Desktop-side model discovery, committed interactive login B through the Host, and then resumed A. The handler queried B's current credential revision, used that revision as the expected value, and successfully committed A's local secret over B. The observed result was ok: true; the commit expected B's revision 2, so the compare-and-swap guard accepted the overwrite rather than rejecting stale work.
The current test only covers a local import with no overlapping login. Please keep Desktop limited to discovering the local raw credential, and let a Host adoption operation own entitlement, supersession/generation, network transport, and the atomic commit alongside interactive login. That gives both paths one ordering authority.
Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.
简体中文
本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。
resolved. |
…b-copilot-device-flow-login # Conflicts: # apps/desktop/src/renderer/settings/provider-connection-detail.tsx # apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
The IPC adapter read this Desktop process's environment before calling the Host. Against a remote Runtime Host that is the wrong authority: a Host whose operator enabled GitHub Copilot enrollment was still refused whenever the Desktop process happened not to set the same variable, and no local answer could have been right, because the environment that governs enrollment is the Host's. The start now always reaches the Host, and its `operation_unavailable` refusal is what the renderer reports — kept as `experimental_disabled` so the user is told the enrollment is off rather than that authorization failed. Generated-by: Claude Code
…t-device-flow-login
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for moving the interactive device flow onto HostOAuthCoordinator; the previous stale-completion root cause is now closed, and token lifetime, refresh, entitlement-before-commit, supersede, and atomic credential commit all follow the existing Host seam. The local-import authority thread remains open, and I found one additional recoverability issue in entitlement classification inline.
AI-assisted review disclosure: OpenAI Codex coordinated three independent exact-head review passes. I verified the retained lifecycle and error-classification paths, existing threads, current-head checks, mergeability, and review state, and I made the final review decision.
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks — one final cancellation edge surfaced in the Copilot 401 replay path. It is bounded and does not corrupt the credential, so I am classifying it as P3 rather than another merge-blocking P2; the two existing P2 items remain the actual readiness gate.
AI-assisted review disclosure: OpenAI Codex delegated an independent protocol and cancellation review. I verified the retained path and severity against this exact head.
…able The entitlement gate turned every `/models` failure into "this account has no Copilot". A timeout, a dropped connection, a 429, a 5xx, or an unreadable body told a paying subscriber they were ineligible and sent them back through a device login that was never the problem — while no credential had been committed and the next attempt would usually have worked. Only a proven refusal is an entitlement answer now: a catalog the account could read that lists nothing, or a deterministic 401/403. Everything else keeps the provider status and surfaces as an authorization failure the user simply retries. The shared 401 replay seam also gains the request's own signal. A caller who cancels while the forced refresh is in flight is released with its cancellation instead of waiting out the refresh timeout and then receiving the stale 401 as though the credential had been rejected; the refresh itself is left to settle, because a spent grant is worth persisting even once nobody wants the replay. Generated-by: Claude Code
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks — reviewed fresh against this head rather than relying on the earlier approval, since that one predates real work on the branch.
What convinced me, in the order I checked it:
The problem is framed correctly. A device authorization grant proves a GitHub account, and a GitHub account is not a Copilot subscription. Calling /models before completeInteractiveOAuthLogin and only committing credentials when reachable models come back is the distinction that keeps a successful sign-in from being mistaken for an entitlement. Conflating those two is the usual way this feature goes wrong, and it is handled here.
The RFC 8628 polling loop covers the cases that matter. authorization_pending, slow_down, access_denied and expired_token all arrive as HTTP 200 and are handled separately, with unknown errors terminating rather than spinning. The loop sleeps by interval first and stops against local expiry rather than trusting the server to say stop. The detail I appreciated most: each token request uses its own signal, because once the grant may already have been consumed, a cancellation that discards the response throws away a credential the user really did authorize — while the sleep and retry boundaries stay cancellable. That distinction is easy to miss.
verification_uri is validated before it is opened: HTTPS only, github.com or a subdomain, no embedded credentials. Handing a server-supplied URL to the system browser unchecked is the classic hole here, and it is closed.
Credentials do not get a private path. Device code, user code, expiry and interval stay in Host memory and never reach the Desktop capability, which only receives the URL and the display user code. Storage goes through the existing canonical vault with the shared refresh/CAS seam, and a GitHub token without a lifetime uses an explicit non-expiring sentinel instead of fabricating a refresh grant. The 401 path reuses the shared refresh-and-replay rather than adding a second one.
On scope, the change removes the duplicated device flow, polling and persistence that lived on the Desktop side instead of adding a parallel implementation, so the authority for this ends up in one place.
On the default being off: gating interactive login behind MAKA_GITHUB_COPILOT_DEVICE_LOGIN_EXPERIMENTAL=1 looks right to me rather than over-cautious. The device flow reuses the GitHub editor OAuth identity, so the party that gave consent and the party holding the credential are not the same — leaving the existing gh/PAT import as the default path while that is true is the honest position, and it is the kind of thing worth stating in the code rather than deciding silently.
No P0–P3 findings. test and audit are both terminal green on this exact head.
Review assisted by AI (Claude Opus 5 and GPT-5.6, reviewed independently). Findings were verified against the files at this head; the reviewers are accountable for them.
…-device-flow-login # Conflicts: # apps/desktop/src/main/__tests__/github-copilot-subscription-service.test.ts # apps/desktop/src/main/oauth/github-copilot-subscription-service.ts # apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts # packages/runtime/src/oauth-provider-contracts.ts
|
Hi — this PR conflicts with current I tested a rebase onto current
These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current Thanks for the contribution — happy to help if any conflict is unclear. AI-assisted maintenance note, not a review. It does not count as the required human review under |
…-device-flow-login # Conflicts: # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts
|
Rebased onto current main — resolved as a merge rather than a rebase to preserve your existing approval, and since the repo squash-merges the branch history won't land on main either way. Happy to redo it as a true rebase if you'd prefer. main had moved again by the time I got to it, so this is against 6b48ed3. Two conflicts, both in packages/runtime-host: the compatibility epoch (this branch held 40, which upstream has since taken for the message-queue mutation ops — Copilot is now 43, and scripts/protocol-epoch-check.mjs passes), and protocol.test.ts, where I kept your new credential-rotation and provider-capacity tests and re-anchored the Copilot assertion to > 42. I also ran npm run write:asf-headers for the five files this branch added before the header gate existed. CI shows one failure: host-kernel.test.js "slow domain work preserves multiplexed requests" timed out on goal.query. That file is untouched by this branch and passes locally (506ms); given a2f2a1a it looks like the same flaky area. Could you re-run the job? I don't have permission to. |
Dismissing a stale approval. This is bound to an old head; the PR has since advanced, is currently conflicting with main, and has a live [P1] outstanding. Because this repository does not dismiss stale reviews automatically, leaving the approval in place would let the PR become mergeable again the moment the conflict is resolved and CI goes green, with that finding still open. Re-approval should happen at whatever head carries the fix.
There was a problem hiding this comment.
English
Incrementally reviewed exact head 0b9ebaf938178cf640309d44064021a8eaa1500c: NO-GO — 2×P1; no lower-severity finding.
- The previously reported borrowed OAuth application-identity boundary remains open. The new merge commit leaves the OAuth contract, identity document, and Host enrollment path byte-identical to the prior reviewed head.
- This head and current
mainindependently assign different incompatible protocol meanings to epoch 44. The current-main peer and this head can therefore pass the compatibility handshake and fail at the firstgithub-copilotlogin request. This is not eligible for a compatible-change declaration because the old closed decoder rejects the new provider.
The Host consolidation remains a genuine simplification: it removes the Desktop-owned credential lifecycle and preserves one Runtime Host authority. The upstream merge introduces no new owner or state path beyond the two findings above.
Verification on this head: Core, Storage, MCP, Runtime, Runtime Host, Computer Use, Desktop main, and Desktop preload built; 253/253 focused affected tests passed; Biome checked 37 changed source/config files and git diff --check passed. The pre-existing full UI build still fails on stale cross-package API shapes.
Gate evidence is unavailable, not merely pending: the PR still conflicts with current main, so GitHub cannot synthesize a merge ref and no pull_request workflow/check-run exists (total_count=0). A rebase must resolve the epoch collision and make the merge result available before CI can provide any gate evidence.
中文
已增量审查 exact head 0b9ebaf938178cf640309d44064021a8eaa1500c:NO-GO——2 条 P1;没有更低级别 finding。
- 上一轮报告的借用 OAuth 应用身份边界仍未关闭。新 merge commit 没有改变 OAuth 契约、身份说明文档或 Host 登录路径,它们与上一轮已审 head 字节一致。
- 当前 head 与当前
main独立地给 epoch 44 分配了两种不同且不兼容的协议语义。因此 current-main peer 与本 head 可以通过兼容性握手,却在第一次github-copilot登录请求时失败。这里不能使用 compatible-change 声明,因为旧的闭合解码器确实会拒绝新 provider。
Host 收口仍是真正的简化:它删除了 Desktop 自己维护的凭证生命周期,并保留单一 Runtime Host 权威。这次 upstream merge 没有在上述两条 finding 之外引入新的 owner 或 state path。
当前 head 的验证结果:Core、Storage、MCP、Runtime、Runtime Host、Computer Use、Desktop main 与 Desktop preload 构建通过;受影响的 focused tests 253/253 通过;Biome 检查 37 个变更源码/配置文件通过,git diff --check 通过。既有的完整 UI 构建仍因过期的跨包 API 形状失败。
门禁证据是无法取得,不是普通的等待中:PR 仍与当前 main 冲突,GitHub 无法生成 merge ref,因此不存在 pull_request workflow/check-run(total_count=0)。必须先 rebase、解决 epoch 冲突并让 merge result 可生成,CI 才能提供任何门禁证据。
| // Increment when the same protocol version no longer guarantees safe Client-Host | ||
| // interoperability. Mismatches are rejected before domain commands are admitted. | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 43 as const; | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 44 as const; |
There was a problem hiding this comment.
English
[P1] Give this incompatible wire shape an epoch strictly beyond current main.
This exact head assigns epoch 44 to adding github-copilot to the closed OAUTH_LOGIN_PROVIDERS decoder, while current main independently assigns epoch 44 to retiring the Session last-used timestamp. A current-main peer and this head therefore advertise the same epoch and pass the compatibility handshake even though the current-main Host rejects a github-copilot login start at the strict provider decoder. The first such request fails mid-session instead of the incompatible pair being refused during handshake.
This is the same-number sibling-branch collision the merge-result guard is designed to reject. Evaluating the guard with base=44, head=44, and the changed OAuth protocol file returns failure; the compatible-change declaration is not valid here because an old peer does reject the new shape. Please rebase onto current main, read its epoch at that time, and assign this incompatible change a strictly greater value (with the corresponding floor test/comment). Do not preselect a fixed number before rebase.
中文
[P1] 请为这项不兼容 wire shape 分配一个严格大于当前 main 的 epoch。
当前 exact head 把 epoch 44 用于向闭合的 OAUTH_LOGIN_PROVIDERS 解码器加入 github-copilot;当前 main 又独立地把 epoch 44 用于移除 Session 最后使用时间。两边因此会声明相同 epoch 并通过兼容性握手,但 current-main Host 会在严格 provider 解码器处拒绝 github-copilot 登录请求。结果是第一次此类请求在 session 中途失败,而不是在握手阶段拒绝这对不兼容 peer。
这正是 merge-result guard 要阻止的“同号 sibling branch”冲突。以 base=44、head=44 和变更的 OAuth protocol 文件执行 guard 会得到失败;这里也不能使用 compatible-change 声明,因为旧 peer 确实会拒绝新形状。请先 rebase 到当前 main,读取当时的 epoch,再为这项不兼容变更分配严格更大的值,并同步更新 floor test/comment。不要在 rebase 前预先认领固定号码。
There was a problem hiding this comment.
Already satisfied by the state pushed, and confirmed by the merge just landed (85abcaae7).
Main is at 47 (the merge you're revalidating against), the branch is at 48, with the floor comment naming the reason. I ran your own criterion — the merge-result guard — against current main: base=47, head=48, pass. The 44-vs-44 collision this thread describes was against an older head; it's stale as of this one.
There was a problem hiding this comment.
[P1] This compatibility collision is present again on exact head 02ee0bf874912102984991b31ff64c588649506e.
The branch assigns epoch 50 to adding github-copilot to the closed OAuth provider decoder. Current main independently assigns epoch 50 to WorkHub Coordination. Those peers therefore pass the epoch handshake, but the current-main Host rejects the first github-copilot login request mid-session because its closed provider set does not contain that value.
The PR is currently dirty, and packages/runtime-host/src/protocol/index.ts is one of the merge conflicts. Please rebase onto live main, then assign this incompatible change an epoch strictly greater than the value on main at that time. A compatibility declaration is not appropriate because the older peer rejects the new wire value.
Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.
简体中文
本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。
…-device-flow-login # Conflicts: # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts
…-device-flow-login # Conflicts: # apps/desktop/src/renderer/settings/provider-connection-detail.tsx # apps/desktop/src/renderer/settings/provider-oauth-section.tsx # packages/runtime-host/src/protocol/index.ts
…b-copilot-device-flow-login
|
Understood, thanks for the explanation. That's resolved now: the merge landed at 85abcaa, the tree is clean against current main (8801edc), and workflow runs are live on this head (audit, test). Will report back once they're green — the one thing I'm watching is host-kernel.test.js's "slow domain work preserves multiplexed requests," which timed out on the previous CI attempt but is untouched by this branch and passes locally; likely the same flaky area as a2f2a1a. |
…-device-flow-login # Conflicts: # apps/desktop/src/main/oauth/github-copilot-subscription-service.ts # packages/runtime-host/src/__tests__/protocol.test.ts # packages/runtime-host/src/protocol/index.ts
|
Heads-up on a number collision, not a review of the change itself. This branch declares Rather than a number, the reliable step is to re-derive it at rebase time, because other branches are moving too: Take the first value above 简体中文提醒一个编号冲突,不是对改动本身的评审。 这个分支声明了 与其给一个数字,更可靠的做法是在 rebase 时重新推导一次,因为其它分支也在移动: 取比 |
…b-copilot-device-flow-login
|
Re-derived rather than reused: 49 turned out to be independently claimed by two other open PRs (#3299, #3651), not just main. Moved to 50, which no open PR currently claims, and matched the floor test's assertion (already |
The Host now owns Copilot enrollment, so the connection detail offers the device sign-in and the catalog panel keeps the local import. The generation story still clicked a `重新导入` button that this branch removed, and upstream apache#3450 could not see the collision: the story and the panel changed on different sides of the merge. Drive the surface that exists. The property under test is unchanged — a same-key Host replacement retires the action guard without discarding the detail route, and the previous generation's late success must not report into the new one — so the story holds the first `completeAuthorization` unsettled across the replacement instead of the first import. Generated-by: Claude Code
…b-copilot-device-flow-login
The merge kept this branch's totals line while taking upstream's rows, so the document claimed 209 files over a 211-file table and the coverage gate read it as stale. Regenerated: 211 files, blocker 0, polish 1, aligned 210. Generated-by: Claude Code
|
Hi @Astro-Han @M4n5ter , could you take a look when getting chance? Thx! |
M4n5ter
left a comment
There was a problem hiding this comment.
NO-GO on exact head 02ee0bf874912102984991b31ff64c588649506e: 2×P1 and 1×P2 remain open.
- [P1] The executable device flow still uses another application's GitHub OAuth identity without an established authorization or compatibility basis. The opt-in flag limits reachability but does not make the consent screen identify Maka or grant Maka permission to receive the token. Details
- [P1] This branch and current
mainboth advertise compatibility epoch 50 for different incompatible protocol changes. The handshake succeeds, then a current-main Host rejects the new provider value mid-session. Details - [P2] A local credential import that starts before a newer interactive login can resume afterward, adopt the newer credential revision, and overwrite that login. A production-module probe reproduced the overwrite. Details
Both exact-head hosted checks are successful, and 32 focused enrollment, coordinator, import, and credential tests pass. Those tests do not cover the reproduced overlapping-login window. The PR is currently mergeable=false / dirty; a local merge-tree reports conflicts in the protocol epoch file and the generated inventory.
The change does remove a real duplicate authority: Desktop no longer owns persistent Copilot credential state, refresh, cancellation, and logout. However, if the OAuth identity requirement cannot be satisfied, the simpler result is to remove the executable device path and retain local credential import instead of keeping the new device-flow model behind an opt-in flag. If the feature remains, three bounded reductions are available: remove the vestigial Desktop open-auth-url and unused refresh-tokens operations, share the GitHub/XAI device-poll lifecycle, and use one Copilot entitlement verifier for both enrollment and local import.
Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.
简体中文
本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 02ee0bf87 myself rather than carrying the earlier approval forward. My reading differs from the automated NO-GO on two of its three items.
The borrowed OAuth identity does not block this PR. Presenting as GitHub's editor client is the existing baseline, not something this branch introduces: GITHUB_COPILOT_COMPAT_HEADERS on main already sends User-Agent: GitHubCopilotChat/0.35.0 and Copilot-Integration-Id: vscode-chat on every Copilot call, including the existing gh import path, with no opt-in. This branch extends the same posture to the device grant and, unlike the baseline, ships it default-off behind a Host-enforced flag with docs/github-copilot-oauth-identity.md recording exactly what is and is not established. Holding this PR to a stricter bar than the code it extends would be asymmetric. The client ID does appear on a consent screen naming the editor application rather than Maka — that is the one part the headers do not cover, and default-off is the right answer to it for now.
The epoch 50 collision is real but is not a finding to fix here. This branch and main both declare 50 for different protocol changes. The PR cannot merge while packages/runtime-host/src/protocol/index.ts is conflicted, and scripts/protocol-epoch-check.mjs runs on the merge result, so a wrong resolution fails CI rather than shipping. One thing to get right when you rebase: take main's 50 as-is and renumber the github-copilot provider note to 51 — do not fold the copilot note under main's 50 entry.
[P3] A remount re-enables local import while a device grant is still polling. useOAuthLoginFlow derives actionBusy from a local pendingAction state and only refreshes the snapshot on mount; it does not rehydrate an in-flight Host attempt. Close and reopen Settings during the device poll and the Import button is enabled again, so the import can commit over the login the user is in the middle of. setRuntimeHostAccountCredential re-reads the credential immediately before its CAS, so it adopts the newer revision and overwrites rather than failing. Both paths commit an entitlement-verified credential and either action recovers it, which is why this is P3. The narrow fix is to seed pendingAction from the Host snapshot on mount.
(The automated review reached this through a direct main-process probe and graded it P2. Through the UI the shared actionBusy guard does hold for the single-window case — the remount path above is the one that reaches it.)
[P3] The title says runetime-host; it becomes the squash commit subject.
Approving on the code. The rebase for the epoch and the two P3s can follow.
AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.
简体中文
在 exact head 02ee0bf87 上自己重审了一遍,没有沿用之前的 approval。我的判断与自动化 NO-GO 在其中两条上不同。
借用 OAuth 身份不阻塞本 PR。 以 GitHub 编辑器客户端身份出现是既有基线,不是本分支引入的:main 上的 GITHUB_COPILOT_COMPAT_HEADERS 已经在每次 Copilot 调用上发送 User-Agent: GitHubCopilotChat/0.35.0 与 Copilot-Integration-Id: vscode-chat,包括现有的 gh 导入路径,且无需任何 opt-in。本分支把同一姿态延伸到设备授权,而且与基线不同,它默认关闭、由 Host 强制门禁,并用 docs/github-copilot-oauth-identity.md 如实记录了哪些成立、哪些不成立。对本 PR 采用比它所扩展的代码更严的标准并不对称。client ID 确实会出现在同意页上,且那里显示的是编辑器应用而非 Maka——这是 header 覆盖不到的一点,目前默认关闭就是对它的正确回答。
epoch 50 撞号确实存在,但不是这里要修的 finding。 本分支与 main 都声明 50,语义不同。只要 packages/runtime-host/src/protocol/index.ts 处于冲突状态,PR 就合不进去;而 scripts/protocol-epoch-check.mjs 跑在 merge result 上,错误的解冲突会让 CI 失败而不是被发布出去。rebase 时有一点要注意:原样保留 main 的 50,把 github-copilot 的说明改成 51——不要把 copilot 的说明并进 main 的 50 条目里。
[P3] remount 会在设备授权仍在轮询时重新启用本地导入。 useOAuthLoginFlow 的 actionBusy 来自本地 pendingAction state,挂载时只刷新快照,不回填 Host 上在途的 attempt。在设备轮询期间关掉再打开 Settings,Import 按钮就重新可用,导入因此可能覆盖用户正在进行中的登录。setRuntimeHostAccountCredential 在 CAS 前会重新读取当前凭据,所以它会采用较新的 revision 直接覆盖,而不是失败。两条路径提交的都是经过 entitlement 校验的凭据,重跑任一动作即可恢复,所以定为 P3。最小的修法是在挂载时用 Host 快照初始化 pendingAction。
(自动化审查是通过直接调用主进程模块复现的,定为 P2。走 UI 时共享的 actionBusy 守卫在单窗口场景下确实成立——真正能触达的是上面这条 remount 路径。)
[P3] 标题写成了 runetime-host,squash 后会成为 commit subject。
代码层面 approve。epoch 的 rebase 和这两条 P3 可以后续处理。
…b-copilot-device-flow-login # Conflicts: # docs/astryx-surface-file-inventory.md # packages/runtime-host/src/protocol/index.ts
…ling `useOAuthLoginFlow` derives its guard from a local `pendingAction`, so a mount knows nothing about an attempt an earlier mount left running in the Host. Close and reopen Settings during a device poll and the Import button is live again: `setRuntimeHostAccountCredential` re-reads the credential immediately before its CAS, so the import adopts the newer revision and commits over the login the user is still completing rather than failing on it. Report the unowned attempt instead of adopting it. The account snapshot says an authorization is running but not which one, so this surface cannot rejoin the attempt — and seeding `pendingAction` from it would disable the sign-in that supersedes a stale attempt, which is the only way out when the earlier surface never got to cancel one. The discovery route stands down; the primary route stays offered. Generated-by: Claude Code
|
Thanks — rebased and addressed both P3s. Epoch. Resolved as you asked: main's 50 and 49 kept verbatim, the github-copilot note renumbered to 51, constant at 51. Not folded under main's entry. The merge-result guard reads 50 → 51. I also reworded the note's second line — it previously justified the number by naming the two open PRs that had claimed 49, which reads as a reservation; it now records that the value is re-derived from main on each rebase. [P3] Import during an in-flight device grant. Confirmed, including the mechanism: activeAttempts lives in the main process, so it outlives a renderer remount, and useOAuthLoginFlow never consults the snapshot it surfaces. I took a narrower fix than seeding pendingAction, and want to flag the deviation rather than let it pass silently. Seeding disables the sign-in button too — and nothing prunes activeAttempts, so an attempt whose surface died before its cancel IPC landed leaves the snapshot reporting authorizing indefinitely. Seeding would make the panel permanently inert with no way out short of an app restart, since a fresh grant is what supersedes a stale attempt. So the flow now exposes hostAttemptPending — an authorization the Host is running that this surface does not own — and only the Copilot import button consults it. The clobbering route stands down; the primary route stays offered; labels and the shared guard are unchanged, so no other provider's panel shifts behavior. Three tests cover adoption on mount, settled states, and not double-reporting an attempt this surface started. Happy to switch to the seeded version if you'd still prefer it — it's a two-line change. [P3] Title fixed. Checks on the merged tree: build, typecheck, lint, format, knip, ASF headers, surface inventory, and the epoch guard all clean; Storybook smoke 195/195; desktop 1520, runtime-host 1251, runtime 3114 tests passing. |
…b-copilot-device-flow-login # Conflicts: # packages/runtime-host/src/protocol/index.ts
…b-copilot-device-flow-login # Conflicts: # packages/runtime-host/src/protocol/index.ts
Two store reads sat between "is a run already in flight for this archive?" and the claim that answers it. A second request for the same day could read no stored archive, then wait on `readConfig` while the first run published and cleared its entry, then find nothing in flight and start its own generation: two model calls for one day, the later archive overwriting the earlier, and a caller that asked to coalesce receiving a different result than the one it believed it had joined. Resolve the model key and the summary first, then check and claim `#inFlight` with no await between them, and move the stored-archive read inside the flight so a caller arriving during that read joins it. CI caught this as `Daily Review conflicts rather than coalescing different generation options` failing on `generatedAt` alone, ten milliseconds apart. No regression test accompanies this: the window can only be held open by delaying a store read, and the authority writer is identity-authenticated with non-configurable methods, so a test cannot substitute a slow one. The existing coalescing test is the detector, and it fails only when the runner is loaded enough to widen the gap on its own. Generated-by: Claude Code
Summary
GitHub Copilot was the only registered
oauth_tokenprovider without a real sign-in path. Desktop only scavenged an existingCOPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN/gh auth token, rejected classic PATs, and left users minting a fine-grained PAT with Copilot Requests.This PR adds the missing RFC 8628 device-flow enrollment in
@maka/runtimeand wires it into Desktop Settings:beginDeviceLoginrequests a user code;completeDeviceLoginpolls until authorizedghimport adopt an account only after it proves usable modelsopenExternalbridge); enrollment pins the URL to github.com and requests onlyread:userImporting an existing
ghcredential remains available as a secondary action.Fixes #3388
Verification
lint/typecheck/ workspace suites not re-run in this PR-opening step; rely on CItestcheckScreenshot
Sign in
Successful GitHub Copilot subscription account import after sign-in:
AI use
Select exactly one:
Tool(s) and scope: GitHub Copilot CLI assisted implementation of the device-flow enrollment, Desktop wiring, tests, commit message cleanup, and PR creation. The human contributor remains responsible for the change.
Checklist
Does this PR entail a change in behavior?