codeburn/app/scripts/snap-grants.test.ts

85 lines
4.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'fs'
import { join } from 'path'
// Snap Store review rejected the first submission because every entry was a
// tool's whole root, and personal-files read is recursive: granting $HOME/.claude
// granted .claude/.credentials.json with it. Each entry must name the log
// directory (or file) the provider actually opens, never the root above it.
// The exceptions below are roots only because the provider reads a file sitting
// directly in them, so no narrower path exists without wildcards.
const ROOT_GRANTS_WITH_NO_NARROWER_FORM = new Set([
'$HOME/.local/share/opencode', // opencode*.db sits in the data dir itself
'$HOME/.local/share/crush', // projects.json sits in the data dir itself
'$HOME/.local/share/kilo', // kilo*.db sits in the data dir itself
'$HOME/.lingtai', // per-agent dirs sit directly under the home root
'$HOME/.lingtai-tui', // registry.jsonl / brief/projects/* sit directly under the global dir
])
const XDG_PARENTS = ['.config', '.local']
function readPlug(name: string): Record<string, unknown> {
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
const plug = pkg.build.snap.plugs.find(
(p: unknown) => typeof p === 'object' && p !== null && name in (p as object),
)
return (plug as Record<string, Record<string, unknown>>)[name]
}
function readGrants(): string[] {
return readPlug('ai-agent-session-logs').read as string[]
}
describe('snap personal-files declaration', () => {
it('names a log path under each tool root, never the root itself', () => {
const bare: string[] = []
for (const entry of readGrants()) {
if (ROOT_GRANTS_WITH_NO_NARROWER_FORM.has(entry)) continue
const segments = entry.replace('$HOME/', '').split('/')
const depth = XDG_PARENTS.includes(segments[0] ?? '') ? 3 : 2
if (segments.length < depth) bare.push(entry)
}
expect(bare).toEqual([])
})
it('requests read only and never a credential file', () => {
// Snap Store review (forum topic 52615): credentials must not ride along
// with the auto-connected session-log plug.
const plug = readPlug('ai-agent-session-logs')
expect(Object.keys(plug).sort()).toEqual(['interface', 'read'])
expect(readGrants().filter(e => e.includes('credential') || e.includes('auth.json')))
.toEqual([])
})
it('keeps the Claude credential file in its own manually-connected plug', () => {
const plug = readPlug('claude-quota-credentials')
expect(plug).toBeDefined()
expect(Object.keys(plug).sort()).toEqual(['interface', 'read'])
expect(plug.interface).toBe('personal-files')
expect(plug.read).toEqual(['$HOME/.claude/.credentials.json'])
})
it('matches the lowercase path the VS Code Copilot parser actually opens', () => {
// Linux is case-sensitive: the parser reads globalStorage/github.copilot-chat
// (copilot.ts getAgentTracesDbPath), never the capitalized GitHub.copilot-chat.
const grants = readGrants()
for (const variant of ['Code', 'Code - Insiders', 'VSCodium']) {
expect(grants).toContain(`$HOME/.config/${variant}/User/globalStorage/github.copilot-chat`)
expect(grants).not.toContain(`$HOME/.config/${variant}/User/globalStorage/GitHub.copilot-chat`)
}
})
it('never covers a credential file sitting beside a narrowed or removed root', () => {
const grants = readGrants()
// pi.ts reads $HOME/.pi/agent/sessions; $HOME/.pi/agent/auth.json is a
// credential file one directory up and must never be reachable.
expect(grants).toContain('$HOME/.pi/agent/sessions')
expect(grants).not.toContain('$HOME/.pi/agent')
expect(grants.some(e => e.startsWith('$HOME/.pi/agent/auth'))).toBe(false)
// $HOME/.config/github-copilot holds the Copilot OAuth files hosts.json /
// apps.json; the only thing under it the parser reads (JetBrains's
// nitrite stores) sits at a variable depth that can't be granted without
// also covering those credentials, so the whole root is dropped.
expect(grants.some(e => e === '$HOME/.config/github-copilot' || e.startsWith('$HOME/.config/github-copilot/'))).toBe(false)
})
})