snap: scope the entries main gained after the tightening

The ported declaration was written against main as of c9e6e2ec. Main has since
added dsh and carries five entries that commit never saw, all of them still bare
tool roots, so the port dropped them rather than reintroduce what the store
rejected. Each is restored at the path its provider opens:

  .dsh/sessions                          dsh.ts reads <DSH_HOME>/sessions only
  .kiro/sessions                         CLI store at sessions/cli, v2 IDE store
                                         at sessions/<hash> — siblings
  .quickwork/{profiles.json,sessions,metrics}
                                         profiles.json names the profile bases;
                                         the legacy layout is sessions/sessions.db
                                         plus metrics/
  .config/Claude/local-agent-mode-sessions
                                         Claude Desktop's local-agent-mode store
  .config/Open Design/{runs,data/runs,namespaces}
                                         the three discovery roots open-design.ts
                                         probes under its data dir

.lingtai and .lingtai-tui stay dropped. A LingTai ledger lives at
.lingtai/<agent>/logs/token_ledger.jsonl, and personal-files has no wildcard for
the agent segment; .lingtai-tui only exists to enumerate project homes that could
not be read anyway. Goose is narrowed to .local/share/goose/sessions, which holds
the only file it opens.

Four entries stay whole roots because the provider reads a file sitting directly
in the root: .config/github-copilot (JetBrains stores nest under a variable
<ide>/<kind>/<storeId>) and .local/share/{opencode,crush,kilo}. The new
app/scripts/snap-grants.test.ts asserts every other entry is at least one level
below its tool root, so a bare root cannot come back unnoticed.
This commit is contained in:
iamtoruk 2026-08-21 03:42:09 -07:00
parent 3168699927
commit c73d8ff6c8
3 changed files with 58 additions and 1 deletions

View file

@ -29,6 +29,9 @@
- **A date-ranged report classifies only the turns it keeps.** Every cached turn went through the turn classifier — category, retries, edit detection, and a full reconstruction of its API calls — before the date slice discarded most of them, so a week view paid to classify all of history to keep a few percent of it. The keep/drop decision is now taken on the raw cached turn and only the survivors are classified, still from their complete call list, with the branch and pull-request carries still walking the full ordered turn list. Output is byte-identical.
- **One rule for every cache file.** `CODEBURN_CACHE_DIR` when set, otherwise `~/.cache/codeburn`. `XDG_CACHE_HOME` is no longer consulted; the sync ledger, the only file that ever honored it, is merged into the canonical location on first read and the legacy copy is retired, so nothing is re-uploaded after the move. (#972)
### Changed (Linux packaging)
- **The snap asks for the log directories it reads, not each tool's whole home.** The first Snap Store submission declared a `personal-files` read of every AI tool's root — `$HOME/.claude`, `$HOME/.codex`, `$HOME/.cursor` and the rest — and that interface is recursive, so it granted read of every credential file those roots hold. Each entry now names the subdirectory the provider actually opens (`.claude/projects`, `.codex/sessions`, `.cline/data`, `.vibe/logs/session`, `.dsh/sessions`, `.kiro/sessions`, `.quickwork/{profiles.json,sessions,metrics}`, `.config/Claude/local-agent-mode-sessions`, `.config/Open Design/{runs,data/runs,namespaces}`), two are single files (`.forge/.forge.db`, `.zcode/cli/db/db.sqlite`), and the editor entries name only the extension folders holding transcripts instead of the editor's whole configuration. Five providers that were missing entirely and would have shown no data are declared — opencode, crush, goose, kilo, kimi-code — and four roots stay roots only because the file the provider opens sits directly in them (`.config/github-copilot`, `.local/share/{opencode,crush,kilo}`). One credential file is now requested openly rather than implicitly: `.claude/.credentials.json`, read-only, for the live plan gauge. Codex's equivalent would need write access to the Codex CLI's own `auth.json` to rotate the token, so neither it nor a Codex root is declared and the Codex live gauge is disabled under `$SNAP`; Codex usage and cost are unaffected, they come from the session rollouts. Two consequences inside the snap: `.lingtai` is dropped, because its per-agent log directory needs a wildcard the interface has no form for, and `optimize`, `context-budget` and `act` no longer see the user-scope `~/.claude/settings.json`, `agents/`, `skills/` and `commands/` — project-scope copies still work through the `home` plug. Nothing outside the snap changes.
### Fixed (Desktop & Menubar)
- **The menubar's copies of your Claude and Codex credentials move out of Application Support and into the login Keychain.** Connecting a provider used to leave the copied OAuth material in `~/Library/Application Support/CodeBurn/*-credentials.v1.json`, written world-readable (0644) because macOS ignores `.completeFileProtection` outside iOS. The copy now lives in a CodeBurn-owned login-Keychain item, and the first read after upgrading migrates the old file: it is reopened with `O_NOFOLLOW`, refused if it is a symlink or not owned by you, repaired to 0600 before a single secret byte is read, written to the Keychain, read back and compared, and only then unlinked — a failed or unverified write leaves the (now 0600) file in place so a retry can still find it, and the next read retries the cleanup. Where both a Keychain item and an old file exist, the one that expires later wins before anything is removed, so an item left behind by a much older build cannot displace a fresher token. Claude's entry no longer stores a refresh token at all — the CLI owns that grant and the menubar never spends it — and any refresh token in a historical blob is dropped on read. Disconnect only reports success once the material is actually gone; if the delete fails it says so and leaves the provider connected so you can retry. Keychain reads are non-interactive and are skipped outright while the login Keychain is locked, so a background quota refresh can never raise an unlock panel. (#1037)
- **First launch no longer asks to control System Events.** The macOS menubar registered its login item by driving System Events over AppleScript, which made macOS put up an Automation consent dialog the first time the app ran. It now registers itself through `SMAppService.mainApp`, an in-process call that needs no Automation grant; there is no AppleScript fallback, so a failure logs and leaves the login item unset rather than bringing the prompt back. The same `codeburn.loginItemRegistered` guard still limits this to the first launch, so a login item you removed by hand stays removed. (#1026)

View file

@ -172,6 +172,7 @@
"$HOME/.cursor/projects",
"$HOME/.cursor/ai-tracking",
"$HOME/.deepseek/sessions",
"$HOME/.dsh/sessions",
"$HOME/.factory/sessions",
"$HOME/.gemini/tmp",
"$HOME/.gemini/antigravity",
@ -182,6 +183,7 @@
"$HOME/.hermes/state.db",
"$HOME/.kimi/sessions",
"$HOME/.kimi-code/sessions",
"$HOME/.kiro/sessions",
"$HOME/.mux/sessions",
"$HOME/.mux/config.json",
"$HOME/.omp/agent",
@ -189,9 +191,13 @@
"$HOME/.openclaw/agents",
"$HOME/.pi/agent",
"$HOME/.qwen/projects",
"$HOME/.quickwork/profiles.json",
"$HOME/.quickwork/sessions",
"$HOME/.quickwork/metrics",
"$HOME/.vibe/logs/session",
"$HOME/.forge/.forge.db",
"$HOME/.zcode/cli/db/db.sqlite",
"$HOME/.config/Claude/local-agent-mode-sessions",
"$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev",
"$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline",
"$HOME/.config/Code/User/globalStorage/kilocode.kilo-code",
@ -209,13 +215,16 @@
"$HOME/.config/Cursor/User/globalStorage/state.vscdb",
"$HOME/.config/Kiro/User/globalStorage/kiro.kiroagent",
"$HOME/.config/Kiro/User/workspaceStorage",
"$HOME/.config/Open Design/runs",
"$HOME/.config/Open Design/data/runs",
"$HOME/.config/Open Design/namespaces",
"$HOME/.kiro-server/data/User/globalStorage/kiro.kiroagent",
"$HOME/.config/github-copilot",
"$HOME/.config/manicode/projects",
"$HOME/.local/share/zed/threads",
"$HOME/.local/share/opencode",
"$HOME/.local/share/crush",
"$HOME/.local/share/goose",
"$HOME/.local/share/goose/sessions",
"$HOME/.local/share/kilo"
]
}

View file

@ -0,0 +1,45 @@
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/.config/github-copilot', // JetBrains stores nest under a variable <ide>/<kind>/<storeId>
'$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
])
const XDG_PARENTS = ['.config', '.local']
function readGrants(): string[] {
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
const plug = pkg.build.snap.plugs.find((p: unknown) => typeof p === 'object')
return plug['ai-agent-session-logs'].read
}
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 one credential file explicitly', () => {
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
const plug = pkg.build.snap.plugs.find((p: unknown) => typeof p === 'object')['ai-agent-session-logs']
expect(Object.keys(plug).sort()).toEqual(['interface', 'read'])
expect(readGrants().filter(e => e.includes('credential') || e.includes('auth.json')))
.toEqual(['$HOME/.claude/.credentials.json'])
})
})