chore(daemon): disambiguate audit observations (#12343)

* chore(daemon): disambiguate audit observations

* fix(daemon): reject future audit protocol roles

* fix(telemetry): protect daemon audit observations
This commit is contained in:
Brennan Benson 2026-08-03 16:28:49 -07:00 committed by GitHub
parent d7fe9d6bcc
commit 13f033f091
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 36 deletions

View File

@ -4,7 +4,9 @@ import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { validate } from '../telemetry/validator'
import { recordAuthenticatedInventory, type DaemonAuditContext } from './daemon-audit-classifier'
import type { ExactDaemonIncarnation } from './daemon-incarnation-evidence'
import { DaemonPtyAdapter } from './daemon-pty-adapter'
import { PROTOCOL_VERSION } from './daemon-protocol-version'
import { DaemonServer } from './daemon-server'
import { getDaemonSocketPath } from './daemon-spawner'
@ -25,6 +27,16 @@ const context: DaemonAuditContext = {
profileScope: '/profile'
}
const exactIncarnation: ExactDaemonIncarnation = {
identity: {
pid: 92_847_561,
startedAtMs: 1_786_000_123_456,
launchNonce: 'private-launch-nonce'
},
linuxStartTicks: '8642097531',
bootId: 'private-boot-id'
}
beforeEach(() => {
trackMock.mockReset()
})
@ -42,12 +54,70 @@ describe('daemon audit eligibility telemetry', () => {
reason: 'authenticated_inventory',
evidence_sources: ['authenticated_inventory'],
protocol_generation: 23,
generation_role: 'legacy',
exact_incarnation: 'unavailable',
process_reason: null
})
expect(props).not.toHaveProperty('exact_incarnation_correlation')
expect(validate('daemon_audit_eligibility', props).ok).toBe(true)
})
it('correlates an exact incarnation across app trackers without raw identity fields', () => {
const currentContext = { ...context, protocolGeneration: PROTOCOL_VERSION }
const firstAppTracker = createDaemonAuditEligibilityTracker()
const secondAppTracker = createDaemonAuditEligibilityTracker()
firstAppTracker(recordAuthenticatedInventory(currentContext, exactIncarnation))
secondAppTracker(recordAuthenticatedInventory(currentContext, exactIncarnation))
secondAppTracker(
recordAuthenticatedInventory(currentContext, {
...exactIncarnation,
identity: { ...exactIncarnation.identity, launchNonce: 'another-private-launch-nonce' }
})
)
const firstProperties = trackMock.mock.calls[0][1]
const secondProperties = trackMock.mock.calls[1][1]
const replacementProperties = trackMock.mock.calls[2][1]
expect(firstProperties).toMatchObject({
generation_role: 'current',
exact_incarnation: 'endpoint-identity-linux-ticks',
exact_incarnation_correlation: 'v1:2751326d0f457808fe11a03ce2f6e732'
})
expect(secondProperties.exact_incarnation_correlation).toBe(
firstProperties.exact_incarnation_correlation
)
expect(replacementProperties.exact_incarnation_correlation).not.toBe(
firstProperties.exact_incarnation_correlation
)
expect(validate('daemon_audit_eligibility', firstProperties).ok).toBe(true)
const serializedProperties = JSON.stringify(firstProperties)
for (const rawIdentity of [
exactIncarnation.identity.launchNonce,
String(exactIncarnation.identity.pid),
String(exactIncarnation.identity.startedAtMs),
exactIncarnation.linuxStartTicks,
exactIncarnation.bootId
]) {
expect(serializedProperties).not.toContain(rawIdentity)
}
})
it('does not mislabel a future protocol generation as legacy', () => {
const trackEligibility = createDaemonAuditEligibilityTracker()
expect(() =>
trackEligibility(
recordAuthenticatedInventory(
{ ...context, protocolGeneration: PROTOCOL_VERSION + 1 },
exactIncarnation
)
)
).not.toThrow()
expect(trackMock).not.toHaveBeenCalled()
})
it('cannot affect callers when telemetry throws', () => {
trackMock.mockImplementation(() => {
throw new Error('transport failed')
@ -83,13 +153,13 @@ describe('daemon audit eligibility telemetry', () => {
for (let index = 0; index < 60; index += 1) {
nowMs += 1_000
trackEligibility(recordAuthenticatedInventory(context, null))
trackEligibility(recordAuthenticatedInventory(context, exactIncarnation))
}
expect(trackMock).toHaveBeenCalledOnce()
nowMs += 5 * 60_000
trackEligibility(recordAuthenticatedInventory(context, null))
trackEligibility(recordAuthenticatedInventory(context, exactIncarnation))
expect(trackMock).toHaveBeenCalledTimes(2)
})

View File

@ -1,15 +1,22 @@
import { createHash } from 'node:crypto'
import { track } from '../telemetry/client'
import type { EventProps } from '../../shared/telemetry-events'
import type { DaemonAuditObservation } from './daemon-audit-classifier'
import { PROTOCOL_VERSION } from './daemon-protocol-version'
// Why: a steady daemon repeats a byte-identical observation on every listProcesses call, so
// repeats are re-sent only as an occasional heartbeat — the shared per-session telemetry
// ceiling is 1,000 events for the whole app and audit data must not crowd it out.
const REPEATED_OBSERVATION_INTERVAL_MS = 5 * 60_000
const INCARNATION_CORRELATION_DOMAIN = 'orca:daemon-audit-eligibility:incarnation:v1'
type AuditEligibilityCommonProperties = Omit<
Extract<EventProps<'daemon_audit_eligibility'>, { exact_incarnation: 'unavailable' }>,
'exact_incarnation'
>
export function trackDaemonAuditEligibility(observation: DaemonAuditObservation): void {
try {
track('daemon_audit_eligibility', auditEligibilityProperties(observation))
track('daemon_audit_eligibility', auditEligibilityProperties(observation, hashLaunchNonce))
} catch {
// Audit telemetry cannot affect daemon availability.
}
@ -22,11 +29,13 @@ export function createDaemonAuditEligibilityTracker(
): (observation: DaemonAuditObservation) => void {
let lastProperties: string | null = null
let lastTrackedAtMs = 0
const correlateLaunchNonce = createLaunchNonceCorrelationCache()
return (observation) => {
// Why: the rate-limit bookkeeping runs inside the daemon's inventory path, so it is guarded
// together with the emit — audit telemetry cannot affect daemon availability.
try {
const properties = JSON.stringify(auditEligibilityProperties(observation))
const eventProperties = auditEligibilityProperties(observation, correlateLaunchNonce)
const properties = JSON.stringify(eventProperties)
const observedAtMs = monotonicNowMs()
const elapsedMs = observedAtMs - lastTrackedAtMs
if (
@ -38,7 +47,7 @@ export function createDaemonAuditEligibilityTracker(
}
lastProperties = properties
lastTrackedAtMs = observedAtMs
trackDaemonAuditEligibility(observation)
track('daemon_audit_eligibility', eventProperties)
} catch {
// Audit telemetry cannot affect daemon availability.
}
@ -46,33 +55,73 @@ export function createDaemonAuditEligibilityTracker(
}
function auditEligibilityProperties(
observation: DaemonAuditObservation
observation: DaemonAuditObservation,
correlateLaunchNonce: (launchNonce: string) => string
): EventProps<'daemon_audit_eligibility'> {
return {
const generationRole = generationRoleForProtocol(observation.context.protocolGeneration)
const commonProperties: AuditEligibilityCommonProperties = {
state: observation.state,
reason: observation.reason,
trigger: observation.trigger,
evidence_sources: [...observation.evidenceSources],
protocol_generation: observation.context.protocolGeneration,
generation_role: generationRole,
provider: observation.context.provider,
endpoint_kind: observation.context.endpointKind,
profile_scope: observation.context.profileScope ? 'configured' : 'unspecified',
exact_incarnation: exactIncarnationKind(observation),
reachability: observation.reachability,
inventory_authority: observation.inventoryAuthority,
process_liveness: observation.processLiveness,
process_reason: observation.processReason,
endpoint_state: observation.endpointState
}
if (!observation.exactIncarnation) {
return { ...commonProperties, exact_incarnation: 'unavailable' }
}
return {
...commonProperties,
exact_incarnation: exactIncarnationKind(observation.exactIncarnation),
exact_incarnation_correlation: correlateLaunchNonce(
observation.exactIncarnation.identity.launchNonce
)
}
}
function generationRoleForProtocol(protocolGeneration: number): 'current' | 'legacy' {
if (protocolGeneration === PROTOCOL_VERSION) {
return 'current'
}
if (protocolGeneration > 0 && protocolGeneration < PROTOCOL_VERSION) {
return 'legacy'
}
throw new Error('unsupported_daemon_audit_protocol_generation')
}
function exactIncarnationKind(
observation: DaemonAuditObservation
): 'endpoint-identity' | 'endpoint-identity-linux-ticks' | 'unavailable' {
if (!observation.exactIncarnation) {
return 'unavailable'
}
return observation.exactIncarnation.linuxStartTicks && observation.exactIncarnation.bootId
exactIncarnation: NonNullable<DaemonAuditObservation['exactIncarnation']>
): 'endpoint-identity' | 'endpoint-identity-linux-ticks' {
return exactIncarnation.linuxStartTicks && exactIncarnation.bootId
? 'endpoint-identity-linux-ticks'
: 'endpoint-identity'
}
function createLaunchNonceCorrelationCache(): (launchNonce: string) => string {
let cachedNonce: string | null = null
let cachedCorrelation = ''
return (launchNonce) => {
if (launchNonce !== cachedNonce) {
cachedNonce = launchNonce
cachedCorrelation = hashLaunchNonce(launchNonce)
}
return cachedCorrelation
}
}
function hashLaunchNonce(launchNonce: string): string {
const digest = createHash('sha256')
.update(INCARNATION_CORRELATION_DOMAIN)
.update('\0')
.update(launchNonce)
.digest('hex')
return `v1:${digest.slice(0, 32)}`
}

View File

@ -160,6 +160,7 @@ describe('telemetry IPC handlers', () => {
count_bucket: 'count_1',
bucket_source: 'crossed_now'
})
handler({}, 'daemon_audit_eligibility', {})
expect(trackMock).not.toHaveBeenCalled()
expect(getCohortAtEmitMock).not.toHaveBeenCalled()
})

View File

@ -24,6 +24,7 @@ let storeRef: Store | null = null
const MAIN_OWNED_TELEMETRY_EVENTS = new Set<EventName>([
'app_starred_orca',
'daemon_audit_eligibility',
'star_nag_outcome',
'feature_interaction_usage_bucket_reached'
])

View File

@ -1,5 +1,7 @@
export const DAEMON_AUDIT_STATE_VALUES = ['present', 'gone', 'unknown'] as const
export const DAEMON_AUDIT_GENERATION_ROLE_VALUES = ['current', 'legacy'] as const
export const DAEMON_AUDIT_TRIGGER_VALUES = [
'inventory_answered',
'inventory_failed',

View File

@ -28,6 +28,7 @@ import {
DAEMON_RETIRE_REASONS
} from './daemon-lifecycle-telemetry'
import {
DAEMON_AUDIT_GENERATION_ROLE_VALUES,
DAEMON_AUDIT_PROCESS_REASON_VALUES,
DAEMON_AUDIT_REASON_VALUES,
DAEMON_AUDIT_STATE_VALUES,
@ -419,28 +420,38 @@ const daemonLifecycleSchema = z.discriminatedUnion('transition', [
.strict()
])
const daemonAuditEligibilitySchema = z
.object({
state: z.enum(DAEMON_AUDIT_STATE_VALUES),
reason: z.enum(DAEMON_AUDIT_REASON_VALUES),
trigger: z.enum(DAEMON_AUDIT_TRIGGER_VALUES),
evidence_sources: z.array(z.enum(DAEMON_EVIDENCE_SOURCE_VALUES)).min(1).max(12),
protocol_generation: z.number().int().positive().max(1_000),
provider: z.literal('local-daemon'),
endpoint_kind: z.enum(['unix-socket', 'windows-named-pipe']),
profile_scope: z.enum(['configured', 'unspecified']),
exact_incarnation: z.enum([
'endpoint-identity',
'endpoint-identity-linux-ticks',
'unavailable'
]),
reachability: z.enum(['authenticated', 'disconnected', 'unknown']),
inventory_authority: z.enum(['authoritative', 'unavailable']),
process_liveness: z.enum(['present', 'gone', 'unknown']),
process_reason: z.enum(DAEMON_AUDIT_PROCESS_REASON_VALUES).nullable(),
endpoint_state: z.enum(['missing', 'named-pipe', 'non-socket', 'socket', 'unknown'])
})
.strict()
const daemonAuditEligibilityBaseSchema = z.object({
state: z.enum(DAEMON_AUDIT_STATE_VALUES),
reason: z.enum(DAEMON_AUDIT_REASON_VALUES),
trigger: z.enum(DAEMON_AUDIT_TRIGGER_VALUES),
evidence_sources: z.array(z.enum(DAEMON_EVIDENCE_SOURCE_VALUES)).min(1).max(12),
protocol_generation: z.number().int().positive().max(1_000),
generation_role: z.enum(DAEMON_AUDIT_GENERATION_ROLE_VALUES),
provider: z.literal('local-daemon'),
endpoint_kind: z.enum(['unix-socket', 'windows-named-pipe']),
profile_scope: z.enum(['configured', 'unspecified']),
reachability: z.enum(['authenticated', 'disconnected', 'unknown']),
inventory_authority: z.enum(['authoritative', 'unavailable']),
process_liveness: z.enum(['present', 'gone', 'unknown']),
process_reason: z.enum(DAEMON_AUDIT_PROCESS_REASON_VALUES).nullable(),
endpoint_state: z.enum(['missing', 'named-pipe', 'non-socket', 'socket', 'unknown'])
})
const daemonAuditEligibilitySchema = z.discriminatedUnion('exact_incarnation', [
daemonAuditEligibilityBaseSchema
.extend({
exact_incarnation: z.literal('endpoint-identity'),
exact_incarnation_correlation: z.string().regex(/^v1:[0-9a-f]{32}$/)
})
.strict(),
daemonAuditEligibilityBaseSchema
.extend({
exact_incarnation: z.literal('endpoint-identity-linux-ticks'),
exact_incarnation_correlation: z.string().regex(/^v1:[0-9a-f]{32}$/)
})
.strict(),
daemonAuditEligibilityBaseSchema.extend({ exact_incarnation: z.literal('unavailable') }).strict()
])
// Rollout signal for granting Codex hook trust via codex app-server RPCs
// instead of Orca's self-computed trusted_hash. `fallback`/`verify_failed`