mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(cli): load extra headers from settings (#1041)
* test: reproduce issue #786 * fix: load extra headers from settings (closes #786) * test: cover extra header precedence and redaction * fix: redact persisted extra headers in diagnostics * test: cover runner extra header identity * fix: restart runner when extra headers change
This commit is contained in:
@@ -12,7 +12,7 @@ import { join } from 'node:path';
|
||||
import { isBunCompiled, projectPath } from '@/projectPath';
|
||||
import { isProcessAlive, isHapiRunnerProcess, killProcess } from '@/utils/process';
|
||||
import { configuration } from '@/configuration';
|
||||
import { hashRunnerCliApiToken, isRunnerStateCompatibleWithIdentity } from './runnerIdentity';
|
||||
import { hashRunnerCliApiToken, hashRunnerExtraHeaders, isRunnerStateCompatibleWithIdentity } from './runnerIdentity';
|
||||
|
||||
export function getInstalledCliMtimeMs(): number | undefined {
|
||||
if (isBunCompiled()) {
|
||||
@@ -219,7 +219,8 @@ export async function isRunnerRunningCurrentlyInstalledHappyVersion(): Promise<b
|
||||
const currentIdentityMatches = isRunnerStateCompatibleWithIdentity(state, {
|
||||
apiUrl: currentApiUrl,
|
||||
machineId: currentMachineId,
|
||||
cliApiTokenHash: hashRunnerCliApiToken(currentCliApiToken)
|
||||
cliApiTokenHash: hashRunnerCliApiToken(currentCliApiToken),
|
||||
extraHeadersHash: hashRunnerExtraHeaders(configuration.extraHeaders)
|
||||
});
|
||||
logger.debug(`[RUNNER CONTROL] Runner identity match: ${currentIdentityMatches}`, {
|
||||
currentApiUrl,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { validateWorkspaceDirectory } from './validateWorkspaceDirectory';
|
||||
import { join } from 'path';
|
||||
import { buildMachineMetadata } from '@/agent/sessionFactory';
|
||||
import { resolveWorkspaceRoots } from '@/utils/workspaceRoot';
|
||||
import { hashRunnerCliApiToken } from './runnerIdentity';
|
||||
import { hashRunnerCliApiToken, hashRunnerExtraHeaders } from './runnerIdentity';
|
||||
import { scheduleCursorModelsPrewarm } from '@/modules/common/cursorModelsPrewarm';
|
||||
|
||||
export async function startRunner(options: { workspaceRoots?: string[] } = {}): Promise<void> {
|
||||
@@ -744,6 +744,7 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
|
||||
startedWithApiUrl: configuration.apiUrl,
|
||||
startedWithMachineId: machineId,
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken(configuration.cliApiToken),
|
||||
startedWithExtraHeadersHash: hashRunnerExtraHeaders(configuration.extraHeaders),
|
||||
startedWithArgv,
|
||||
startedWithVersionHandoffDisabled,
|
||||
runnerLogPath: logger.logFilePath
|
||||
@@ -1021,6 +1022,7 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
|
||||
startedWithApiUrl: fileState.startedWithApiUrl,
|
||||
startedWithMachineId: fileState.startedWithMachineId,
|
||||
startedWithCliApiTokenHash: fileState.startedWithCliApiTokenHash,
|
||||
startedWithExtraHeadersHash: fileState.startedWithExtraHeadersHash,
|
||||
startedWithArgv,
|
||||
startedWithVersionHandoffDisabled,
|
||||
lastHeartbeat: new Date().toLocaleString(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hashRunnerCliApiToken, isRunnerStateCompatibleWithIdentity } from './runnerIdentity'
|
||||
import {
|
||||
hashRunnerCliApiToken,
|
||||
hashRunnerExtraHeaders,
|
||||
isRunnerStateCompatibleWithIdentity
|
||||
} from './runnerIdentity'
|
||||
|
||||
describe('runnerIdentity', () => {
|
||||
it('matches when api url, machine id, token hash all same', () => {
|
||||
@@ -49,6 +53,113 @@ describe('runnerIdentity', () => {
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects reused runner when extra headers changed', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: 'old-headers-hash'
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: 'new-headers-hash'
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('hashes equivalent extra headers identically regardless of insertion order', () => {
|
||||
const first = hashRunnerExtraHeaders({
|
||||
'X-Second': 'two',
|
||||
'X-First': 'one'
|
||||
})
|
||||
const second = hashRunnerExtraHeaders({
|
||||
'X-First': 'one',
|
||||
'X-Second': 'two'
|
||||
})
|
||||
|
||||
expect(first).toBe(second)
|
||||
expect(first).toMatch(/^[a-f0-9]{64}$/)
|
||||
})
|
||||
|
||||
it('matches equivalent extra headers with different insertion order', () => {
|
||||
const first = hashRunnerExtraHeaders({
|
||||
'X-Second': 'two',
|
||||
'X-First': 'one'
|
||||
})
|
||||
const second = hashRunnerExtraHeaders({
|
||||
'X-First': 'one',
|
||||
'X-Second': 'two'
|
||||
})
|
||||
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: first
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: second
|
||||
}
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps legacy runner state compatible when no extra headers are configured', () => {
|
||||
expect(hashRunnerExtraHeaders({})).toBeUndefined()
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token')
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: undefined
|
||||
}
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects legacy runner state when extra headers are now configured', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token')
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: 'configured-headers-hash'
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects runner state with headers when current headers are empty', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: 'configured-headers-hash'
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: undefined
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects reused runner when current machine id is missing', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ export type RunnerConnectionIdentity = {
|
||||
apiUrl: string
|
||||
machineId?: string
|
||||
cliApiTokenHash?: string
|
||||
extraHeadersHash?: string
|
||||
}
|
||||
|
||||
export function hashRunnerCliApiToken(token: string | null | undefined): string | undefined {
|
||||
@@ -15,10 +16,25 @@ export function hashRunnerCliApiToken(token: string | null | undefined): string
|
||||
return createHash('sha256').update(trimmed).digest('hex')
|
||||
}
|
||||
|
||||
export function hashRunnerExtraHeaders(
|
||||
headers: Readonly<Record<string, string>> | null | undefined
|
||||
): string | undefined {
|
||||
const entries = Object.entries(headers ?? {})
|
||||
if (entries.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
||||
return createHash('sha256').update(JSON.stringify(entries)).digest('hex')
|
||||
}
|
||||
|
||||
export function isRunnerStateCompatibleWithIdentity(
|
||||
state: Pick<
|
||||
RunnerLocallyPersistedState,
|
||||
'startedWithApiUrl' | 'startedWithMachineId' | 'startedWithCliApiTokenHash'
|
||||
| 'startedWithApiUrl'
|
||||
| 'startedWithMachineId'
|
||||
| 'startedWithCliApiTokenHash'
|
||||
| 'startedWithExtraHeadersHash'
|
||||
>,
|
||||
current: RunnerConnectionIdentity
|
||||
): boolean {
|
||||
@@ -34,5 +50,9 @@ export function isRunnerStateCompatibleWithIdentity(
|
||||
return false
|
||||
}
|
||||
|
||||
if (state.startedWithExtraHeadersHash !== current.extraHeadersHash) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user