test(startup): pin the keyring-deferral gates and the dropped-lease save

This commit is contained in:
Brennan Benson
2026-08-28 06:01:02 -07:00
parent ba87908a7a
commit 5debca3e29
4 changed files with 96 additions and 3 deletions
+2 -1
View File
@@ -39,6 +39,7 @@ import { ElectronSecretStore } from './host/electron-secret-store'
import { reportSecretProtectionGap } from './host/secret-protection-report'
import { initSessionParseCachePersistence } from './ai-vault/session-parse-cache-persistence'
import { deferUntilFirstWindowShown } from './window/defer-until-first-window-shown'
import { shouldDeferKeyringProbe } from './startup/keyring-probe-deferral'
import { ensureActiveOrcaProfile, initOrcaProfilePaths } from './orca-profiles/profile-index-store'
import { getOrcaCloudAuthConfig } from './orca-profiles/profile-cloud-auth-config'
import { getProfileUserDataPath } from './orca-profiles/profile-storage-paths'
@@ -2347,7 +2348,7 @@ void app.whenReady().then(async () => {
// OS keyring — on Linux a blocking D-Bus round trip that a locked keyring never answers, run
// here with no window on screen to explain the wait (STA-5782). Serve is excluded: it opens no
// window, so deferring would move the block past the point clients can already pair.
const deferKeyringProbe = process.platform === 'linux' && !isServeMode
const deferKeyringProbe = shouldDeferKeyringProbe({ platform: process.platform, isServeMode })
store = new Store({
dataFile: activeOrcaProfile.dataFile,
storageAuthority: isServeMode ? 'runtime' : 'desktop',
@@ -37,6 +37,7 @@ function seal(plaintext: string): string {
}
let probes = 0
let encryptions = 0
function installBlockingKeyring(): void {
setSecretStore({
@@ -51,13 +52,16 @@ function installBlockingKeyring(): void {
}
return true
},
encryptString: (plainText) => Buffer.from(`encrypted:${plainText}`, 'utf-8'),
// Why a nonce: a real keyring seals with a fresh IV, so re-encrypting a value yields
// different bytes. Without that, a save that decrypted and re-encrypted a secret would be
// indistinguishable from one that preserved the stored ciphertext untouched.
encryptString: (plainText) => Buffer.from(`encrypted:${plainText}#${++encryptions}`, 'utf-8'),
decryptString: (cipher) => {
const decoded = cipher.toString('utf-8')
if (!decoded.startsWith('encrypted:')) {
throw new Error('invalid ciphertext')
}
return decoded.slice('encrypted:'.length)
return decoded.slice('encrypted:'.length).replace(/#\d+$/, '')
},
describeProtectionGap: () => null
})
@@ -90,6 +94,7 @@ describe('deferred protected-secret hydration', () => {
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-deferred-secret-'))
probes = 0
encryptions = 0
installBlockingKeyring()
})
@@ -199,6 +204,47 @@ describe('deferred protected-secret hydration', () => {
store.freezeWrites()
})
it('persists the drop when hydration finds an unusable SSH owner lease', async () => {
// Why: the lease decodes to a value normalization rejects, so hydration drops the record.
// Dropping it in memory only would resurrect it next launch and re-present a lease the
// relay will refuse, so the drop has to reach disk on hydration's own save.
writeDataFile({
sshPtyConsumerRecoveries: [
{
targetId: 'target-1',
clientInstanceId: 'client-1',
serverBuildId: 'build-1',
clientGeneration: 1,
ownerGeneration: 1,
ownerLease: seal('')
}
]
})
const store = createStore({ deferKeyringProbe: true })
// Why flush first: load-time migrations schedule their own save, and a hydration that
// scheduled nothing would still reach disk on the back of it.
store.flushOrThrow()
expect(
(JSON.parse(readFileSync(dataFile(), 'utf-8')) as { sshPtyConsumerRecoveries: unknown[] })
.sshPtyConsumerRecoveries
).toHaveLength(1)
const result = store.hydrateDeferredProtectedSecrets()
expect(result.needsSave).toBe(true)
await vi.waitFor(
async () => {
await store.waitForPendingWrite()
const saved = JSON.parse(readFileSync(dataFile(), 'utf-8')) as {
sshPtyConsumerRecoveries: unknown[]
}
expect(saved.sshPtyConsumerRecoveries).toEqual([])
},
{ timeout: 10_000, interval: 100 }
)
store.freezeWrites()
})
it('probes inline when the host opens no window, so nothing pairs against a stalled main thread', () => {
// Why pinned: headless serve advertises readiness with no window to defer behind, and an
// earlier deferral moved this block to after clients could already connect (STA-5765).
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { shouldDeferKeyringProbe } from './keyring-probe-deferral'
describe('shouldDeferKeyringProbe', () => {
it('defers on Linux desktop, the only host where the probe can block forever', () => {
expect(shouldDeferKeyringProbe({ platform: 'linux', isServeMode: false })).toBe(true)
})
it('never defers in serve mode, which advertises readiness with no window to defer behind', () => {
// Why pinned separately from the platform gate: this is the regression a reviewer caught on
// the sibling leg (STA-5765) — deferring here moves the block past the point clients pair.
expect(shouldDeferKeyringProbe({ platform: 'linux', isServeMode: true })).toBe(false)
})
it('never defers on macOS or Windows, whose keychains answer promptly', () => {
// Why both listed: a platform gate that accidentally widened to every desktop would withhold
// secrets for the whole pre-window startup on hosts that never had the hazard.
expect(shouldDeferKeyringProbe({ platform: 'darwin', isServeMode: false })).toBe(false)
expect(shouldDeferKeyringProbe({ platform: 'win32', isServeMode: false })).toBe(false)
})
it('never defers on a non-Linux serve host either', () => {
expect(shouldDeferKeyringProbe({ platform: 'darwin', isServeMode: true })).toBe(false)
})
})
@@ -0,0 +1,21 @@
/** The host facts that decide whether the profile load may skip the OS keyring probe. */
export type KeyringProbeDeferralHost = {
platform: NodeJS.Platform
isServeMode: boolean
}
/**
* Whether the profile load may answer "keyring unavailable" without asking the keyring,
* leaving every protected slot sealed until the first window has painted (STA-5782).
*
* Linux only: `safeStorage.isEncryptionAvailable()` is a blocking D-Bus round trip there, and a
* keyring that is present but locked with no unlock prompter accepts the call and never replies.
* macOS and Windows answer promptly, so deferring would only widen the window in which secrets
* are withheld for no gain.
*
* Never in serve mode: a headless host opens no window to defer behind, so the probe would land
* after `printServeReady`, and a client could pair against a main thread already stalled on it.
*/
export function shouldDeferKeyringProbe(host: KeyringProbeDeferralHost): boolean {
return host.platform === 'linux' && !host.isServeMode
}