fix(sync): address project privacy review

This commit is contained in:
Aditya Vikram Singh 2026-08-24 20:09:27 +05:30
parent 33dc05bb90
commit cc721f00a7
6 changed files with 97 additions and 20 deletions

View file

@ -62,13 +62,14 @@ sqliteDescribe('Goose sync project provenance', () => {
created_timestamp INTEGER
);
`)
db.prepare(`
const insertSession = db.prepare(`
INSERT INTO sessions (
id, name, working_dir, created_at, updated_at,
accumulated_input_tokens, accumulated_output_tokens,
provider_name, model_config_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
`)
insertSession.run(
'goose-session-1',
'LLM-authored session title',
'/Users/alice/company/private-widget',
@ -79,18 +80,33 @@ sqliteDescribe('Goose sync project provenance', () => {
'openai',
JSON.stringify({ model_name: 'gpt-5.4' }),
)
insertSession.run(
'goose-container-session',
'Container session title must stay local',
'/sessions/synthetic-customer-secret',
'2026-08-23T11:00:00.000Z',
'2026-08-23T11:01:00.000Z',
50,
10,
'openai',
JSON.stringify({ model_name: 'gpt-5.4' }),
)
db.close()
const provider = createGooseProvider()
const sources = await provider.discoverSessions()
expect(sources).toHaveLength(1)
expect(sources).toHaveLength(2)
const calls = []
for await (const providerCall of provider.createSessionParser(sources[0]!, new Set()).parse()) calls.push(providerCall)
expect(calls).toHaveLength(1)
expect(calls[0]!.workingDirectory).toBe('/Users/alice/company/private-widget')
for (const source of sources) {
for await (const providerCall of provider.createSessionParser(source, new Set()).parse()) calls.push(providerCall)
}
expect(calls).toHaveLength(2)
const trusted = calls.find(call => call.sessionId === 'goose-session-1')!
const container = calls.find(call => call.sessionId === 'goose-container-session')!
expect(trusted.workingDirectory).toBe('/Users/alice/company/private-widget')
expect(container.workingDirectory).toBe('/sessions/synthetic-customer-secret')
const raw = calls[0]!
const parsed: ParsedApiCall = {
const toParsed = (raw: typeof trusted): ParsedApiCall => ({
provider: raw.provider,
model: raw.model,
usage: {
@ -113,19 +129,23 @@ sqliteDescribe('Goose sync project provenance', () => {
timestamp: raw.timestamp,
bashCommands: raw.bashCommands,
deduplicationKey: raw.deduplicationKey,
}
const payload = buildOtlpPayload([{
call: parsed,
})
const payload = buildOtlpPayload(calls.map(raw => ({
call: toParsed(raw),
sessionId: raw.sessionId,
project: sources[0]!.project,
workingDirectory: raw.workingDirectory,
}])
const span = payload.resourceSpans[0]!.scopeSpans[0]!.spans[0]!
const attributes = Object.fromEntries(span.attributes.map(attribute => [attribute.key, attribute.value]))
})))
const spans = payload.resourceSpans[0]!.scopeSpans[0]!.spans
const trustedSpan = spans.find(span => span.spanId === deriveSpanId(trusted.deduplicationKey))!
const containerSpan = spans.find(span => span.spanId === deriveSpanId(container.deduplicationKey))!
const trustedAttributes = Object.fromEntries(trustedSpan.attributes.map(attribute => [attribute.key, attribute.value]))
const containerAttributes = Object.fromEntries(containerSpan.attributes.map(attribute => [attribute.key, attribute.value]))
expect(attributes['ai.project']).toEqual({ stringValue: 'private-widget' })
expect(trustedAttributes['ai.project']).toEqual({ stringValue: 'private-widget' })
expect(containerAttributes['ai.project']).toBeUndefined()
expect(JSON.stringify(payload)).not.toContain('/Users/alice')
expect(JSON.stringify(payload)).not.toContain('synthetic-customer-secret')
expect(JSON.stringify(payload)).not.toContain('LLM-authored session title')
expect(span.spanId).toBe(deriveSpanId(raw.deduplicationKey))
expect(JSON.stringify(payload)).not.toContain('Container session title must stay local')
})
})

View file

@ -76,6 +76,7 @@ describe('sync project privacy boundary', () => {
'/mnt/c/Users/alice',
'/var/home/alice',
'/net/home/alice',
'/sessions/synthetic-customer-secret',
'\\\\server\\Users\\alice',
'D:\\Profiles\\alice',
])('omits ai.project when cwd provenance is absent or unsafe: %s', cwd => {
@ -116,6 +117,26 @@ describe('sync identifier privacy boundary', () => {
expect(wire).not.toContain('synthetic-secret')
expect(wire).not.toContain('alice@example.com')
})
it('preserves real routed provider, model, and tool identifiers', () => {
const payload = buildOtlpPayload([usage('/workspace/widget', {
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
tools: ['orcarouter/openai-compatible', 'mcp__github__search'],
})])
const span = payload.resourceSpans[0]!.scopeSpans[0]!.spans[0]!
const attrs = Object.fromEntries(span.attributes.map(attribute => [attribute.key, attribute.value]))
expect(attrs['ai.provider']).toEqual({ stringValue: 'openrouter' })
expect(attrs['ai.model']).toEqual({ stringValue: 'anthropic/claude-sonnet-4.6' })
expect(attrs['ai.tools']).toEqual({
arrayValue: { values: [
{ stringValue: 'orcarouter/openai-compatible' },
{ stringValue: 'mcp__github__search' },
] },
})
expect(span.name).toBe('openrouter/anthropic/claude-sonnet-4.6')
})
})
describe('sync attribution project privacy boundary', () => {
@ -150,4 +171,19 @@ describe('sync attribution project privacy boundary', () => {
expect(attrs['ai.project']).toBeUndefined()
expect(JSON.stringify(payload)).not.toContain('/Users/alice/secret')
})
it('accepts owner/repo identities and rejects credential-directory repo labels', () => {
const [legitimate, credentialShaped] = flattenAttributionRecords([
record({ repo: 'acme/widget' }),
record({ sessionId: 'privacy-session-2', repo: 'acme/.ssh' }),
])
expect(legitimate?.project).toBe('widget')
expect(credentialShaped?.project).toBeUndefined()
const payload = buildAttributionOtlpPayload([legitimate!, credentialShaped!])
const spans = payload.resourceSpans[0]!.scopeSpans[0]!.spans
const credentialAttributes = Object.fromEntries(spans[1]!.attributes
.map(attribute => [attribute.key, attribute.value]))
expect(credentialAttributes['ai.project']).toBeUndefined()
})
})