mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(desktop): retry the update check anonymously when the env token is rejected
Wiring GITHUB_TOKEN/GH_TOKEN into fetchGitHubApi made a stale or revoked
token fail the check closed ('api.github.com answered HTTP 401.') on
machines where the anonymous request had always worked. An authenticated
401 now logs once that the env token was rejected and repeats the request
without the Authorization header; every other status is surfaced as before.
envTokenRejected is the pure gate so the decision is unit-tested.
This commit is contained in:
1 parent
c7d4d32800
commit
e79ba2d7f2
3 files changed
+50
-3
No files matched your search
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
|
||||
import { envTokenRejected, githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
|
||||
|
||||
// The exact headers the anonymous update check sends today; the change must
|
||||
// leave this shape byte-identical when no token is configured.
|
||||
@@ -32,3 +32,13 @@ test('anonymous sends no Authorization key at all; a token adds the `token` sche
|
||||
// into every later request that builds from it.
|
||||
assert.equal('Authorization' in UPDATE_CHECK_HEADERS, false)
|
||||
})
|
||||
|
||||
test('only an authenticated 401 counts as the env token being rejected', () => {
|
||||
// The anonymous-retry gate: a stale GITHUB_TOKEN must fall back to the
|
||||
// anonymous request that worked before the token was wired in.
|
||||
assert.equal(envTokenRejected({ statusCode: 401, authenticated: true }), true)
|
||||
assert.equal(envTokenRejected({ statusCode: 401, authenticated: false }), false)
|
||||
assert.equal(envTokenRejected({ statusCode: 403, authenticated: true }), false)
|
||||
assert.equal(envTokenRejected({ statusCode: 429, authenticated: true }), false)
|
||||
assert.equal(envTokenRejected(null), false)
|
||||
})
|
||||
@@ -43,3 +43,14 @@ export function githubApiHeaders(base: Record<string, string>, token?: string |
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* True when api.github.com rejected the env token itself (HTTP 401 on an
|
||||
* authenticated call). A stale or revoked GITHUB_TOKEN must not fail the
|
||||
* update check closed — the caller retries anonymously, which is exactly what
|
||||
* worked before the token was wired in. Anonymous 401s and every other status
|
||||
* are not the token's fault and are surfaced as-is.
|
||||
*/
|
||||
export function envTokenRejected(error: { statusCode?: number; authenticated?: boolean } | null | undefined): boolean {
|
||||
return error?.statusCode === 401 && error?.authenticated === true
|
||||
}
|
||||
@@ -217,7 +217,7 @@ import {
|
||||
import { startGatewaysAfterUpdateAbort, stopGatewayBeforeUpdate } from './gateway-stop-before-update'
|
||||
import { probeGatewayWebSocket } from './gateway-ws-probe'
|
||||
import { registerGitIpc } from './git-ipc'
|
||||
import { githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
|
||||
import { envTokenRejected, githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
|
||||
import { desktopBackendSpawnEnv, guestOnboardingEnabled, skipIntroEnabled } from './guest-onboarding'
|
||||
import { readAndConsumeHandoffResult } from './handoff-result'
|
||||
import {
|
||||
@@ -3337,9 +3337,35 @@ async function checkUpdatesViaLsRemote({ updateRoot, branch, currentSha }) {
|
||||
// GITHUB_TOKEN / GH_TOKEN from the environment, when present, moves the call
|
||||
// from the anonymous 60/hour-per-IP budget to the token's 5,000/hour one; the
|
||||
// header shape is otherwise unchanged. Read per request, never stored.
|
||||
function fetchGitHubApi(url, accept = 'application/vnd.github+json') {
|
||||
//
|
||||
// A token GitHub rejects (401: expired, revoked, malformed) must not turn a
|
||||
// check that worked anonymously into a hard failure, so the call is retried
|
||||
// once without it; the rejection is logged once per process.
|
||||
let warnedRejectedGitHubToken = false
|
||||
|
||||
async function fetchGitHubApi(url, accept = 'application/vnd.github+json') {
|
||||
const token = githubTokenFromEnv(process.env)
|
||||
|
||||
try {
|
||||
return await fetchGitHubApiOnce(url, accept, token)
|
||||
} catch (error) {
|
||||
if (!envTokenRejected(error)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!warnedRejectedGitHubToken) {
|
||||
warnedRejectedGitHubToken = true
|
||||
rememberLog(
|
||||
'[updates] api.github.com rejected the GITHUB_TOKEN / GH_TOKEN from the environment (HTTP 401); ' +
|
||||
'retrying the update check anonymously'
|
||||
)
|
||||
}
|
||||
|
||||
return fetchGitHubApiOnce(url, accept, null)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchGitHubApiOnce(url, accept, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(
|
||||
url,
|
||||
|
||||
Reference in new issue
Block a user