Webhook relay multi-user hardening: GitHub App device-flow auth + repo-permission gate#684
Conversation
…ssion gate
Worker: /github/repos/:owner/:repo/{events,status} now require a Bearer
GitHub token with push/maintain/admin on the repo (GET /repos permissions,
fallback /user + collaborator permission). Read-only/public tokens get 403
with no event or status leakage. Legacy /projects routes unchanged.
Clients: hosted relay reads authenticate with an expiring GitHub App user
token minted via GitHub device flow (never the user's PAT/gh token). New
shared githubAppUserAuthService factory (desktop + ade-cli) owns the token
store, single-flight refresh with clear-auth epoch fence, device-session
lifecycle, and deduped audit logging. Settings panel gains the Authorize
ADE device-flow UI (copyable code, auto-renew capped at 3); new typed
`ade github app-auth login|status|clear` for headless setups.
Live-verified against GitHub: device flow enabled, read-only app user
token reports true repo role, collaborator fallback works, pending polls
return HTTP 200.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
@copilot review but do not make fixes |
📝 WalkthroughWalkthroughThis PR adds GitHub App user device-flow authentication across desktop, CLI, IPC, and renderer surfaces, wires hosted relay status checks to app-user tokens, and rewrites webhook-relay repository authorization to use permission checks with collaborator fallback. ChangesGitHub App Device Auth Implementation
Webhook Relay Permission-Based Authorization
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx (1)
263-319: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winShow the GitHub authorization state before installation states.
Line 313 only returns “Authorize GitHub” when the installation check is already in
error, so an unauthenticated user can still see “Configured”, “Installed”, or “Not installed” from earlier branches while the new auth CTA is shown. Move the!appAuthorizedbranch ahead of the installation-status branches so the header/badge matches the required auth action.Proposed fix
function statusView(status: GitHubAppInstallationStatus | null, loading: boolean, appAuthorized: boolean): { label: string; color: string; description: (repoLabel: string | null) => string; } { if (loading && !status) { return { label: "Checking", color: COLORS.warning, description: () => "Checking whether this project already has the ADE GitHub App installed.", }; } + if (!appAuthorized) { + return { + label: "Authorize GitHub", + color: COLORS.warning, + description: () => "Authorize ADE with GitHub to enable instant PR updates for this repo.", + }; + } if (status?.relayConfigured && status.webhookState === "deleted") { return { label: "Webhook off", color: COLORS.warning, description: (repoLabel) => @@ } if (status?.state === "error") { - if (!appAuthorized) { - return { - label: "Authorize GitHub", - color: COLORS.warning, - description: () => "Authorize ADE with GitHub to enable instant PR updates for this repo.", - }; - } return { label: "Check failed", color: COLORS.danger, description: () => status.error ?? "ADE could not check GitHub App status. Existing GitHub auth remains the fallback.",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx` around lines 263 - 319, The statusView() logic in GitHubAppInstallPanel.tsx should prioritize authorization state before any installation-related labels so unauthenticated users do not see “Configured,” “Installed,” or other status badges from earlier branches. Move the !appAuthorized handling ahead of the status?.relayConfigured / status?.installed checks, and keep the “Authorize GitHub” CTA as the first applicable return so the displayed header matches the required auth action.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 15632-15645: In cli.ts, the polling loop in the device auth flow
currently always sleeps for the full GitHub interval before checking the
deadline, which can push execution past --max-wait. Update the while loop around
runGithubAction("pollAppUserDeviceAuth") to cap the sleep duration by the
remaining time until deadlineMs, and keep the existing timeout branch as the
final guard. Use the existing deadlineMs, intervalSec, and sessionId flow in
this auth polling block so the loop times out exactly when the configured wait
is exceeded.
In `@apps/desktop/src/main/services/github/githubAppUserAuthService.ts`:
- Around line 172-173: The refresh path in githubAppUserAuthService can still
persist an old token after a newer authorization has been stored, because
authEpoch only changes in clearAuth. Update the token replacement flow in
pollDeviceAuth and the refresh handler that calls persistAppUserTokenRecord so
stale in-flight refreshes are rejected, either by bumping authEpoch whenever a
token is replaced or by checking the currently active refresh token before
persisting. Keep the epoch/token guard consistent in the then((refreshed) =>
...) logic and any other persistence paths referenced by the review.
In `@apps/desktop/src/main/services/github/githubService.test.ts`:
- Around line 1578-1612: The in-flight refresh path in githubService should not
yield a usable token after clearAppUserAuth() invalidates the current auth
epoch. Update the token refresh flow in getAppUserTokenForRelay (and any helper
that persists the refreshed token) to detect epoch/status mismatch and reject or
suppress the refreshed response instead of returning "ghu_new_token". Adjust
this test to assert the promise is rejected or resolves without a token, and
keep the credentialStore and getAppUserAuthStatus assertions verifying auth
stays cleared.
In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 7912-7930: The IPC handlers in registerIpc.ts for
githubStartAppUserDeviceAuth and githubPollAppUserDeviceAuth currently forward
requests directly to the GitHub service without any abuse control. Add
per-sender/per-channel throttling around these handlers in ipcMain.handle, and
enforce a small cap on active device auth sessions in the GitHub service so
repeated renderer calls cannot spam GitHub or accumulate pending sessions. Use
the existing getCtx, startAppUserDeviceAuth, and pollAppUserDeviceAuth entry
points to place the guards close to the IPC boundary.
---
Outside diff comments:
In `@apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx`:
- Around line 263-319: The statusView() logic in GitHubAppInstallPanel.tsx
should prioritize authorization state before any installation-related labels so
unauthenticated users do not see “Configured,” “Installed,” or other status
badges from earlier branches. Move the !appAuthorized handling ahead of the
status?.relayConfigured / status?.installed checks, and keep the “Authorize
GitHub” CTA as the first applicable return so the displayed header matches the
required auth action.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 78295a53-d056-408b-a55c-8998eb70f939
⛔ Files ignored due to path filters (3)
docs/ARCHITECTURE.mdis excluded by!docs/**docs/features/automations/README.mdis excluded by!docs/**docs/features/onboarding-and-settings/README.mdis excluded by!docs/**
📒 Files selected for processing (21)
apps/ade-cli/README.mdapps/ade-cli/src/cli.tsapps/ade-cli/src/headlessLinearServices.tsapps/desktop/src/main/services/adeActions/registry.tsapps/desktop/src/main/services/automations/automationIngressService.test.tsapps/desktop/src/main/services/automations/automationIngressService.tsapps/desktop/src/main/services/github/githubAppUserAuth.tsapps/desktop/src/main/services/github/githubAppUserAuthService.tsapps/desktop/src/main/services/github/githubRelayConfig.tsapps/desktop/src/main/services/github/githubService.test.tsapps/desktop/src/main/services/github/githubService.tsapps/desktop/src/main/services/ipc/registerIpc.tsapps/desktop/src/preload/global.d.tsapps/desktop/src/preload/preload.tsapps/desktop/src/renderer/browserMock.tsapps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsxapps/desktop/src/shared/ipc.tsapps/desktop/src/shared/types/git.tsapps/webhook-relay/README.mdapps/webhook-relay/src/relay.tsapps/webhook-relay/test/relay.test.ts
| while (true) { | ||
| if (Number.isFinite(deadlineMs) && Date.now() >= deadlineMs) { | ||
| process.stderr.write("GitHub device authorization timed out.\n"); | ||
| const status = await runGithubAction("getAppUserAuthStatus"); | ||
| return { | ||
| output: formatOutput( | ||
| { ...status, status: "expired", error: "timed_out" }, | ||
| options, | ||
| ), | ||
| exitCode: 1, | ||
| }; | ||
| } | ||
| await sleep(Math.max(1, intervalSec) * 1000); | ||
| const poll = await runGithubAction("pollAppUserDeviceAuth", { sessionId }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Bound the sleep to the remaining deadline.
With --max-wait shorter than GitHub’s polling interval, Line 15644 sleeps the full interval and can poll after the configured deadline instead of timing out at N seconds.
🐛 Proposed fix
- await sleep(Math.max(1, intervalSec) * 1000);
+ const sleepMs = Math.max(1, intervalSec) * 1000;
+ const waitMs = Number.isFinite(deadlineMs)
+ ? Math.min(sleepMs, Math.max(0, deadlineMs - Date.now()))
+ : sleepMs;
+ if (waitMs > 0) await sleep(waitMs);
+ if (Number.isFinite(deadlineMs) && Date.now() >= deadlineMs) {
+ continue;
+ }
const poll = await runGithubAction("pollAppUserDeviceAuth", { sessionId });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| while (true) { | |
| if (Number.isFinite(deadlineMs) && Date.now() >= deadlineMs) { | |
| process.stderr.write("GitHub device authorization timed out.\n"); | |
| const status = await runGithubAction("getAppUserAuthStatus"); | |
| return { | |
| output: formatOutput( | |
| { ...status, status: "expired", error: "timed_out" }, | |
| options, | |
| ), | |
| exitCode: 1, | |
| }; | |
| } | |
| await sleep(Math.max(1, intervalSec) * 1000); | |
| const poll = await runGithubAction("pollAppUserDeviceAuth", { sessionId }); | |
| while (true) { | |
| if (Number.isFinite(deadlineMs) && Date.now() >= deadlineMs) { | |
| process.stderr.write("GitHub device authorization timed out.\n"); | |
| const status = await runGithubAction("getAppUserAuthStatus"); | |
| return { | |
| output: formatOutput( | |
| { ...status, status: "expired", error: "timed_out" }, | |
| options, | |
| ), | |
| exitCode: 1, | |
| }; | |
| } | |
| const sleepMs = Math.max(1, intervalSec) * 1000; | |
| const waitMs = Number.isFinite(deadlineMs) | |
| ? Math.min(sleepMs, Math.max(0, deadlineMs - Date.now())) | |
| : sleepMs; | |
| if (waitMs > 0) await sleep(waitMs); | |
| if (Number.isFinite(deadlineMs) && Date.now() >= deadlineMs) { | |
| continue; | |
| } | |
| const poll = await runGithubAction("pollAppUserDeviceAuth", { sessionId }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/ade-cli/src/cli.ts` around lines 15632 - 15645, In cli.ts, the polling
loop in the device auth flow currently always sleeps for the full GitHub
interval before checking the deadline, which can push execution past --max-wait.
Update the while loop around runGithubAction("pollAppUserDeviceAuth") to cap the
sleep duration by the remaining time until deadlineMs, and keep the existing
timeout branch as the final guard. Use the existing deadlineMs, intervalSec, and
sessionId flow in this auth polling block so the loop times out exactly when the
configured wait is exceeded.
| ipcMain.handle(IPC.githubStartAppUserDeviceAuth, async (): Promise<GitHubAppDeviceAuthStartResult> => { | ||
| const ctx = getCtx(); | ||
| return await ctx.githubService.startAppUserDeviceAuth(); | ||
| }); | ||
|
|
||
| ipcMain.handle( | ||
| IPC.githubPollAppUserDeviceAuth, | ||
| async (_event, arg: { sessionId?: string }): Promise<GitHubAppDeviceAuthPollResult> => { | ||
| const ctx = getCtx(); | ||
| const sessionId = arg?.sessionId?.trim() ?? ""; | ||
| if (!sessionId) { | ||
| return { | ||
| status: "error", | ||
| intervalSec: null, | ||
| message: "GitHub device authorization session id is required.", | ||
| authStatus: ctx.githubService.getAppUserAuthStatus(), | ||
| }; | ||
| } | ||
| return await ctx.githubService.pollAppUserDeviceAuth({ sessionId }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Throttle device-flow IPC before it reaches GitHub.
githubStartAppUserDeviceAuth and githubPollAppUserDeviceAuth delegate straight to network-backed device-flow calls, and the service keeps device sessions in memory. Add per-sender/channel throttling and ideally a small active-session cap so a renderer bug cannot spam GitHub or grow pending sessions. As per path instructions, apps/desktop/src/** Electron changes should be checked for IPC security.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/main/services/ipc/registerIpc.ts` around lines 7912 - 7930,
The IPC handlers in registerIpc.ts for githubStartAppUserDeviceAuth and
githubPollAppUserDeviceAuth currently forward requests directly to the GitHub
service without any abuse control. Add per-sender/per-channel throttling around
these handlers in ipcMain.handle, and enforce a small cap on active device auth
sessions in the GitHub service so repeated renderer calls cannot spam GitHub or
accumulate pending sessions. Use the existing getCtx, startAppUserDeviceAuth,
and pollAppUserDeviceAuth entry points to place the guards close to the IPC
boundary.
Source: Path instructions
…on cap - Bump authEpoch when device auth stores a new token so a stale in-flight refresh can never overwrite newer auth state (CodeRabbit) - On epoch mismatch after refresh, re-resolve against current auth state: cleared -> reject, re-authorized -> return the new token (CodeRabbit) - Treat missing refreshTokenExpiresAt as unknown, not expired (Greptile) - Cap pending device-auth sessions at 5, evicting oldest (CodeRabbit) - Drop unreachable post-prune expiry branch in pollDeviceAuth (Greptile) - Cap the relay audit dedup set at 500 keys (Greptile) - Bound `ade github app-auth login` poll sleep to the --max-wait deadline (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f5505b700
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const authStatus = await window.ade.github.getAppUserAuthStatus?.().catch(() => null); | ||
| setAppAuth(authStatus ?? null); | ||
| setStatus(await window.ade.github.getAppInstallationStatus({ forceRefresh })); |
There was a problem hiding this comment.
Refresh auth state after status checks clear it
When a stored App user token is expired or otherwise rejected during getAppInstallationStatus, the main service can clear that credential before returning the auth-required status, but this component keeps the earlier tokenStored: true value read here. In that expired-token scenario the panel shows a check failure while hiding the “Authorize ADE” button until a second refresh/remount, so re-read the auth status after the status call or update it from the failure path.
Useful? React with 👍 / 👎.
The status check can clear an expired stored token; reading auth state first left the panel showing a stale tokenStored=true with the Authorize button hidden (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8ce3263be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| <div style={actionRowStyle}> | ||
| {status?.installed ? null : ( | ||
| {!appAuthorized ? ( |
There was a problem hiding this comment.
Keep reauthorization available for failing app-user tokens
When the stored app-user token belongs to a different GitHub account, lacks write access to the current repo, or is otherwise stale, the hosted relay status check returns an auth/permission error while getAppUserAuthStatus can still report tokenStored: true; this guard then hides the only Authorize ADE CTA, leaving Settings with no way to switch accounts or re-authorize from the panel. Please keep a reauthorize/clear path available on auth or permission failures, not only when no token is stored.
Useful? React with 👍 / 👎.
A stored token from the wrong account or without repo access produced a status error with the Authorize button hidden, leaving no recovery path in Settings (Codex P2). Status errors now surface a Re-authorize button that restarts the device flow and replaces the stored token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 617fe96d21
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| githubAppInstallationStatusCache.clear(); | ||
| }, | ||
| () => | ||
| callProjectRuntimeActionOr("github", "pollAppUserDeviceAuth", { args }, () => |
There was a problem hiding this comment.
Pass the session ID at the action argument level
When a project runtime is bound, this call does not reach the IPC fallback; it invokes the GitHub action through run_ade_action, which passes the contents of request.args directly to githubService.pollAppUserDeviceAuth. Wrapping the payload as { args } makes the service receive { args: { sessionId } } instead of { sessionId }, so pollDeviceAuth sees an undefined session id and reports the device session as not found, preventing the desktop Authorize ADE flow from completing for normal open projects.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified against the actual dispatch chain — this is the correct envelope, not a double-wrap. callProjectRuntimeActionOr("github", "pollAppUserDeviceAuth", { args }) produces RemoteRuntimeActionRequest.args = { sessionId }; localRuntimeConnectionPool.callActionForRootUncoalesced then maps it to run_ade_action as arguments: { domain, action, args: request.args } (localRuntimeConnectionPool.ts ~line 995), and the daemon calls service.pollAppUserDeviceAuth(safeObject(toolArgs.args)) → { sessionId } (adeRpcServer.ts ~3443). Same pattern as the long-standing listRepoAutolinks/listRepoLabels wiring on this route. Also runtime-verified end-to-end via ade actions run github.pollAppUserDeviceAuth during CLI parity testing.
Summary
/github/repos/:owner/:repo/{events,status}now require a Bearer GitHub token whose user has push/maintain/admin on the repo (GET /repospermissions; fallback/user+ collaborator permission). Read-only/public tokens get 403 with no events/status leakage. Legacy self-hosted/projects/:projectIdroutes andade_proj_tokens unchanged.githubAppUserAuthServicefactory (desktop + ade-cli) owns thegithub.appUserToken.v1store, single-flight refresh with a clear-auth epoch fence, device-session lifecycle, and deduped audit logs.ade github app-auth login|status|clearfor headless/brain setups (CTO-gated; token values never printed).github.getAppUserAuthStatus/startAppUserDeviceAuth/pollAppUserDeviceAuth/clearAppUserAuthwired through preload + daemon action registry (device-auth trio CTO-only);getAppUserTokenForRelaydeliberately not RPC-exposed.Verification
authorization_pending= HTTP 200; app permissions confirmed all-read.Post-merge note
The Cloudflare Worker must be redeployed (
wrangler deployin apps/webhook-relay) for the new 403 gate to take effect on the hosted relay.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
login,status, andclearsubcommands.Bug Fixes
Greptile Summary
This PR adds GitHub App device-flow authentication to the ADE desktop app and CLI, and tightens the webhook relay worker so that
/github/repos/:owner/:repo/{events,status}routes now require a Bearer token with push/maintain/admin access rather than any valid repo-read token. The relay gains a two-tier permission check (permissions hash fromGET /repos→ collaborator-permission fallback), and the desktop/CLI both adopt a new sharedcreateGitHubAppUserAuthServicefactory that handles device-flow sessions, single-flight token refresh, and epoch-fenced auth-clear.assertGitHubRepoAuthorizednow callsrepoPermissionsAllowWebhookReadon thepermissionshash, falling back to a three-API-call collaborator-permission check when the hash is absent; read-only tokens receive a 403 at both the events and status endpoints.githubAppUserAuthServiceencapsulates device-flow session state, single-flight refresh with an epoch fence to prevent stale overwrites, and a null-safe refresh-token expiry check (addressed from prior review round).ade github app-auth login|status|clearcommands and a device-flow panel in the GitHub settings screen complete the headless and interactive authorization surfaces.Confidence Score: 4/5
Safe to merge for the relay worker and auth service; the preload has a one-line arg-wrapping mistake that breaks device-auth polling when a project runtime is attached to the renderer.
The relay permission gate, auth service, CLI flow, IPC handlers, and UI panel are all correct. One issue in
preload.tspasses{ args: { sessionId } }instead of{ sessionId }tocallProjectRuntimeActionOrforpollAppUserDeviceAuth. In runtimes where the runtime path is preferred over IPC, device-flow polling would always return "session not found", silently stalling the authorization UI for any user going through the Settings panel with a runtime attached.apps/desktop/src/preload/preload.ts — the
pollAppUserDeviceAuthargs wrapping.Important Files Changed
pollAppUserDeviceAuthpasses{ args }(nested) instead ofargsdirectly tocallProjectRuntimeActionOr, breaking the runtime action path for device-flow polling.assertGitHubRepoAuthorizedwith a permissions-hash check plus a 3-step collaborator-permission fallback; helper functions are cleanly separated and tests cover the new branches.authorization_pending(HTTP 200 + error field) and the slow-down backoff.resolveHostedGitHubRelayAuthToken;fetchGitHubAppInstallationStatuscorrectly gates on the app-user token for hosted relays.cancelledflag to guard all post-await state updates.getTokenOrThrowtogetAppUserTokenForRelay; hosted-auth resolution and audit logging are correctly gated on the non-legacy route.forceRefreshforwarding added togetAppInstallationStatus.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant UI as GitHubAppInstallPanel participant Preload as preload.ts participant IPC as Electron IPC participant GS as githubService participant Auth as githubAppUserAuthService participant GH as GitHub API UI->>Preload: startAppUserDeviceAuth() Preload->>IPC: invoke(githubStartAppUserDeviceAuth) IPC->>GS: startAppUserDeviceAuth() GS->>Auth: startDeviceAuth() Auth->>GH: POST /login/device/code GH-->>Auth: "{ device_code, user_code, expires_in }" Auth-->>GS: "{ sessionId, userCode, verificationUri }" GS-->>UI: DeviceAuthStartResult loop Poll until authorized/expired/denied UI->>Preload: "pollAppUserDeviceAuth({ sessionId })" Preload->>IPC: invoke(githubPollAppUserDeviceAuth, args) IPC->>GS: "pollAppUserDeviceAuth({ sessionId })" GS->>Auth: "pollDeviceAuth({ sessionId })" Auth->>GH: POST /login/oauth/access_token GH-->>Auth: "{ access_token } or { error: authorization_pending }" Auth-->>UI: "DeviceAuthPollResult { status }" end UI->>Preload: getAppInstallationStatus() Preload->>IPC: invoke(githubGetAppInstallationStatus) IPC->>GS: getAppInstallationStatus() GS->>Auth: getValidTokenForRelay() Note over Auth: Refresh if expiring (single-flight) Auth-->>GS: app user token GS->>Relay: GET /github/repos/:owner/:repo/status Relay->>GH: GET /repos/:owner/:repo Note over Relay,GH: Checks permissions.push/maintain/admin or falls back to collaborator API GH-->>Relay: repo + permissions Relay-->>UI: installation status%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant UI as GitHubAppInstallPanel participant Preload as preload.ts participant IPC as Electron IPC participant GS as githubService participant Auth as githubAppUserAuthService participant GH as GitHub API UI->>Preload: startAppUserDeviceAuth() Preload->>IPC: invoke(githubStartAppUserDeviceAuth) IPC->>GS: startAppUserDeviceAuth() GS->>Auth: startDeviceAuth() Auth->>GH: POST /login/device/code GH-->>Auth: "{ device_code, user_code, expires_in }" Auth-->>GS: "{ sessionId, userCode, verificationUri }" GS-->>UI: DeviceAuthStartResult loop Poll until authorized/expired/denied UI->>Preload: "pollAppUserDeviceAuth({ sessionId })" Preload->>IPC: invoke(githubPollAppUserDeviceAuth, args) IPC->>GS: "pollAppUserDeviceAuth({ sessionId })" GS->>Auth: "pollDeviceAuth({ sessionId })" Auth->>GH: POST /login/oauth/access_token GH-->>Auth: "{ access_token } or { error: authorization_pending }" Auth-->>UI: "DeviceAuthPollResult { status }" end UI->>Preload: getAppInstallationStatus() Preload->>IPC: invoke(githubGetAppInstallationStatus) IPC->>GS: getAppInstallationStatus() GS->>Auth: getValidTokenForRelay() Note over Auth: Refresh if expiring (single-flight) Auth-->>GS: app user token GS->>Relay: GET /github/repos/:owner/:repo/status Relay->>GH: GET /repos/:owner/:repo Note over Relay,GH: Checks permissions.push/maintain/admin or falls back to collaborator API GH-->>Relay: repo + permissions Relay-->>UI: installation statusComments Outside Diff (2)
General comment
timeout 25s npm --prefix apps/ade-cli run dev -- --role cto github app-auth clear --texttimed out with exit code 124 and no CLI result. The claimed contract saysclearis a typed CTO-gated command that removes stored authorization; a CTO invocation should complete with status metadata rather than hanging at the CLI boundary.github app-auth clearplan inapps/ade-cli/src/cli.tsroutes to the runtime action, but the CTO/headless execution path did not return to the CLI in validation. This appears anchored in the CLI action wiring/connection lifecycle forgithub.clearAppUserAuthwhen elevated as CTO.github app-auth clearcommand uses the same bounded, returning runtime path as status and closes the CLI connection after theclearAppUserAuthresult is formatted. Add a targeted CLI/integration test that runs--role cto github app-auth clear --textand asserts a zero exit plus no token output.General comment
github app-auth login --max-wait 1 --textdid not return a clear CTO-gate error before the validation process was terminated (exit 143). It should fail fast with the documentedrequires --role ctousage error rather than hanging/continuing into connection work.apps/ade-cli/src/cli.tsonly converts elevated-role errors to a login-specific usage error after creating a connection and attempting thestartAppUserDeviceAuthaction; the non-CTO path did not reliably return that error at the CLI boundary during validation.buildGithubPlanor at the beginning ofrunGithubAppLoginfor login/start and return the documented usage error before opening runtime/device-flow state. Cover with a CLI test forade github app-auth login --max-wait 1 --textwithout--role cto.Reviews (4): Last reviewed commit: "Address review: keep re-authorize CTA vi..." | Re-trigger Greptile