From 9a6d8a843c420fbfdbd7f07d6e461a8108091d04 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 18 Aug 2026 17:07:26 -0700 Subject: [PATCH 1/7] feat(host-service): share account-agnostic Claude state across usage profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Secondary Claude profiles (the CLAUDE_CONFIG_DIR dirs behind the Usage tab's account switching, #6585) each kept their own conversation history, prompt history, and settings — switching accounts meant losing --resume, skills, and config. Token-swapping into one config dir was considered and rejected: refresh-token rotation plus running sessions rewriting the credential slot mid-session make it unsafe (see claude.ts's read-only hard rule). Instead, profile provisioning now links account-agnostic state into ~/.claude so every login sees one history and one config set, while identity (.claude.json, credential stores) and runtime dirs stay per-profile: - Session state (projects, sessions, session-env, file-history, shell-snapshots, todos, paste-cache, tasks, plans, transcripts) is merged file-by-file, then the dir becomes a symlink. The swap is rename -> symlink -> merge: renames preserve inodes, so live sessions keep writing through the new link. Conflicts wait in a .superset-merge dir and are retried on the next run. - Config (agents, commands, skills, hooks, plugins, CLAUDE.md) links only when the profile has nothing of its own. - history.jsonl is appended into main's then linked; settings.json is backed up beside the link (or promoted when main has none). Runs on prepareClaudeProfile and setDefaultAccount, before the managed hook merge so hooks land in the shared settings file. The usage-history scanner now realpath-dedupes its scan roots: a profile whose projects/ symlinks into ~/.claude would otherwise get the multi-GB transcript tree walked and parsed once per profile. --- .../trpc/router/usage/history/aggregate.ts | 37 ++- .../trpc/router/usage/profile-share.test.ts | 209 +++++++++++++++ .../src/trpc/router/usage/profile-share.ts | 244 ++++++++++++++++++ .../src/trpc/router/usage/usage.ts | 16 +- 4 files changed, 491 insertions(+), 15 deletions(-) create mode 100644 packages/host-service/src/trpc/router/usage/profile-share.test.ts create mode 100644 packages/host-service/src/trpc/router/usage/profile-share.ts diff --git a/packages/host-service/src/trpc/router/usage/history/aggregate.ts b/packages/host-service/src/trpc/router/usage/history/aggregate.ts index c6535a22dd5..5a8dcfc4060 100644 --- a/packages/host-service/src/trpc/router/usage/history/aggregate.ts +++ b/packages/host-service/src/trpc/router/usage/history/aggregate.ts @@ -1,3 +1,4 @@ +import { realpath } from "node:fs/promises"; import { homedir } from "node:os"; import { basename, join } from "node:path"; import { discoverClaudeProfiles, discoverCodexHomes } from "../profiles"; @@ -188,18 +189,34 @@ export async function computeUsageHistory( ]); for (const profile of claudeProfiles) claudeHomes.add(profile.configDir); - const [claudeFileGroups, codexFileGroups] = await Promise.all([ - Promise.all( - [...claudeHomes].map((root) => - collectLogFiles(join(root, "projects"), days + 1), - ), - ), - Promise.all( - codexHomes.map((codexHome) => - collectLogFiles(join(codexHome.home, "sessions"), days + 1), - ), + // Shared-history profiles symlink their projects/ into ~/.claude (see + // profile-share.ts), so two homes can name the same tree under different + // paths — resolve every scan root and dedupe by real path, or the tree + // gets walked and parsed once per profile. + const resolveRoots = async (roots: string[]): Promise => { + const resolved = await Promise.all( + roots.map(async (root) => { + try { + return await realpath(root); + } catch { + return null; // Dir absent — collectLogFiles would find nothing. + } + }), + ); + return [ + ...new Set(resolved.filter((root): root is string => root !== null)), + ]; + }; + const [claudeRoots, codexRoots] = await Promise.all([ + resolveRoots([...claudeHomes].map((root) => join(root, "projects"))), + resolveRoots( + codexHomes.map((codexHome) => join(codexHome.home, "sessions")), ), ]); + const [claudeFileGroups, codexFileGroups] = await Promise.all([ + Promise.all(claudeRoots.map((root) => collectLogFiles(root, days + 1))), + Promise.all(codexRoots.map((root) => collectLogFiles(root, days + 1))), + ]); const claudeFiles = dedupeLogFiles(claudeFileGroups.flat()); const codexFiles = dedupeLogFiles(codexFileGroups.flat()); diff --git a/packages/host-service/src/trpc/router/usage/profile-share.test.ts b/packages/host-service/src/trpc/router/usage/profile-share.test.ts new file mode 100644 index 00000000000..49a102c3764 --- /dev/null +++ b/packages/host-service/src/trpc/router/usage/profile-share.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from "bun:test"; +import { + lstatSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + readlinkSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { shareableProfileDir, shareClaudeProfileState } from "./profile-share"; + +function makeDirs(): { profile: string; main: string } { + const root = mkdtempSync(join(tmpdir(), "claude-profile-share-")); + const profile = join(root, "profile"); + const main = join(root, "main"); + mkdirSync(profile); + mkdirSync(main); + return { profile, main }; +} + +function isLinkTo(path: string, target: string): boolean { + return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target; +} + +describe("shareableProfileDir", () => { + it("refuses the default homes and the main home itself", () => { + const home = homedir(); + const main = join(home, ".claude"); + expect(shareableProfileDir(home, main)).toBeNull(); + expect(shareableProfileDir(join(home, ".claude"), main)).toBeNull(); + expect( + shareableProfileDir(join(home, ".config", "claude"), main), + ).toBeNull(); + expect( + shareableProfileDir("/tmp/custom-main", "/tmp/custom-main"), + ).toBeNull(); + }); + + it("accepts an ordinary profile dir", () => { + const home = homedir(); + expect( + shareableProfileDir(join(home, ".claude-work"), join(home, ".claude")), + ).toBe(join(home, ".claude-work")); + }); +}); + +describe("shareClaudeProfileState", () => { + it("links a fresh profile's config and session entries into main", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(main, "settings.json"), "{}"); + shareClaudeProfileState(profile, main); + for (const name of ["agents", "skills", "projects", "sessions"]) { + expect(isLinkTo(join(profile, name), join(main, name))).toBe(true); + expect(lstatSync(join(main, name)).isDirectory()).toBe(true); + } + expect( + isLinkTo(join(profile, "history.jsonl"), join(main, "history.jsonl")), + ).toBe(true); + expect( + isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), + ).toBe(true); + }); + + it("merges existing session trees into main before linking", () => { + const { profile, main } = makeDirs(); + mkdirSync(join(profile, "projects", "-repo-a"), { recursive: true }); + writeFileSync(join(profile, "projects", "-repo-a", "s1.jsonl"), "a"); + mkdirSync(join(main, "projects", "-repo-b"), { recursive: true }); + writeFileSync(join(main, "projects", "-repo-b", "s2.jsonl"), "b"); + shareClaudeProfileState(profile, main); + expect(isLinkTo(join(profile, "projects"), join(main, "projects"))).toBe( + true, + ); + expect( + readFileSync(join(main, "projects", "-repo-a", "s1.jsonl"), "utf-8"), + ).toBe("a"); + expect( + readFileSync(join(main, "projects", "-repo-b", "s2.jsonl"), "utf-8"), + ).toBe("b"); + // The merged file is reachable through the profile's own path too. + expect( + readFileSync(join(profile, "projects", "-repo-a", "s1.jsonl"), "utf-8"), + ).toBe("a"); + }); + + it("leaves conflicting files behind in the pending dir and still links", () => { + const { profile, main } = makeDirs(); + mkdirSync(join(profile, "projects", "-repo"), { recursive: true }); + writeFileSync(join(profile, "projects", "-repo", "s.jsonl"), "profile"); + mkdirSync(join(main, "projects", "-repo"), { recursive: true }); + writeFileSync(join(main, "projects", "-repo", "s.jsonl"), "main"); + shareClaudeProfileState(profile, main); + expect(isLinkTo(join(profile, "projects"), join(main, "projects"))).toBe( + true, + ); + expect( + readFileSync(join(main, "projects", "-repo", "s.jsonl"), "utf-8"), + ).toBe("main"); + expect( + readFileSync( + join(profile, "projects.superset-merge", "-repo", "s.jsonl"), + "utf-8", + ), + ).toBe("profile"); + }); + + it("flushes a pending dir left by an interrupted merge", () => { + const { profile, main } = makeDirs(); + mkdirSync(join(profile, "projects.superset-merge", "-repo"), { + recursive: true, + }); + writeFileSync( + join(profile, "projects.superset-merge", "-repo", "s.jsonl"), + "leftover", + ); + symlinkSync(join(main, "projects"), join(profile, "projects")); + mkdirSync(join(main, "projects")); + shareClaudeProfileState(profile, main); + expect( + readFileSync(join(main, "projects", "-repo", "s.jsonl"), "utf-8"), + ).toBe("leftover"); + expect(readdirSync(profile)).not.toContain("projects.superset-merge"); + }); + + it("appends the profile's prompt history to main's", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(main, "history.jsonl"), '{"m":1}\n'); + writeFileSync(join(profile, "history.jsonl"), '{"p":1}\n'); + shareClaudeProfileState(profile, main); + expect( + isLinkTo(join(profile, "history.jsonl"), join(main, "history.jsonl")), + ).toBe(true); + expect(readFileSync(join(main, "history.jsonl"), "utf-8")).toBe( + '{"m":1}\n{"p":1}\n', + ); + }); + + it("backs up divergent settings beside the link", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(main, "settings.json"), '{"main":true}'); + writeFileSync(join(profile, "settings.json"), '{"profile":true}'); + shareClaudeProfileState(profile, main); + expect( + isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), + ).toBe(true); + const backup = readdirSync(profile).find((name) => + name.startsWith("settings.json.pre-share-"), + ); + expect(backup).toBeDefined(); + expect(readFileSync(join(profile, backup as string), "utf-8")).toBe( + '{"profile":true}', + ); + }); + + it("promotes the profile's settings when main has none", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(profile, "settings.json"), '{"profile":true}'); + shareClaudeProfileState(profile, main); + expect(readFileSync(join(main, "settings.json"), "utf-8")).toBe( + '{"profile":true}', + ); + expect( + isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), + ).toBe(true); + }); + + it("respects config a profile deliberately diverged", () => { + const { profile, main } = makeDirs(); + mkdirSync(join(profile, "agents")); + writeFileSync(join(profile, "agents", "mine.md"), "custom"); + writeFileSync(join(profile, "CLAUDE.md"), "profile memory"); + shareClaudeProfileState(profile, main); + expect(lstatSync(join(profile, "agents")).isSymbolicLink()).toBe(false); + expect(lstatSync(join(profile, "CLAUDE.md")).isSymbolicLink()).toBe(false); + expect(readFileSync(join(profile, "CLAUDE.md"), "utf-8")).toBe( + "profile memory", + ); + }); + + it("leaves existing symlinks alone, wherever they point", () => { + const { profile, main } = makeDirs(); + const elsewhere = join(profile, "elsewhere"); + mkdirSync(elsewhere); + symlinkSync(elsewhere, join(profile, "skills")); + shareClaudeProfileState(profile, main); + expect(readlinkSync(join(profile, "skills"))).toBe(elsewhere); + }); + + it("never touches identity or runtime entries", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(profile, ".claude.json"), '{"oauthAccount":{}}'); + writeFileSync(join(profile, ".credentials.json"), "secret"); + mkdirSync(join(profile, "daemon")); + shareClaudeProfileState(profile, main); + expect(lstatSync(join(profile, ".claude.json")).isSymbolicLink()).toBe( + false, + ); + expect(lstatSync(join(profile, ".credentials.json")).isSymbolicLink()).toBe( + false, + ); + expect(lstatSync(join(profile, "daemon")).isSymbolicLink()).toBe(false); + expect(readdirSync(main)).not.toContain(".claude.json"); + expect(readdirSync(main)).not.toContain(".credentials.json"); + }); +}); diff --git a/packages/host-service/src/trpc/router/usage/profile-share.ts b/packages/host-service/src/trpc/router/usage/profile-share.ts new file mode 100644 index 00000000000..4eae08585ac --- /dev/null +++ b/packages/host-service/src/trpc/router/usage/profile-share.ts @@ -0,0 +1,244 @@ +/** + * Shares account-agnostic Claude state between a secondary profile dir and + * the default `~/.claude` home via symlinks, so every login sees one + * conversation history, one prompt history, and one config set — switching + * accounts stops meaning losing `--resume`, skills, and settings. + * + * Identity stays per-profile by design: `.claude.json` (oauthAccount, + * per-project trust, MCP auth) and the credential stores are never touched. + * Runtime dirs (daemon/, cache/, telemetry/, backups/) stay local too — the + * daemon's locks and sockets must not be shared between CLIs. + * + * Existing real state is merged, not clobbered. Session trees are renamed + * aside, the symlink lands immediately, and files then move into `~/.claude` + * one by one: renames preserve inodes, so a live session's open transcripts + * stay valid and its paths resolve through the new link. The prompt history + * is appended; a divergent settings.json is backed up beside the link. + * Anything that cannot merge safely is left where it is. + */ + +import { + appendFileSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + renameSync, + rmdirSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { homedir, platform } from "node:os"; +import { join, resolve } from "node:path"; + +/** Config the user authors once and expects everywhere. Linked only when + * the profile has nothing of its own (absent or empty) — a profile that + * deliberately diverged keeps its version. */ +const CONFIG_DIRS = ["agents", "commands", "skills", "hooks", "plugins"]; +const CONFIG_FILES = ["CLAUDE.md"]; + +/** Session-scoped state keyed by session UUID or cwd slug: safe to merge + * file-by-file, since two profiles' sessions collide no more than two + * concurrent sessions in one dir already do. */ +const SESSION_DIRS = [ + "projects", + "sessions", + "session-env", + "file-history", + "shell-snapshots", + "todos", + "paste-cache", + "tasks", + "plans", + "transcripts", +]; + +const MERGE_SUFFIX = ".superset-merge"; + +/** Resolved profile dir, or null when it IS a default home (never share a + * default home into itself, and never operate on `~`). */ +export function shareableProfileDir( + configDir: string, + mainHome: string, +): string | null { + const resolved = resolve(configDir); + const home = homedir(); + const excluded = new Set([ + home, + join(home, ".claude"), + join(home, ".config"), + join(home, ".config", "claude"), + resolve(mainHome), + ]); + return excluded.has(resolved) ? null : resolved; +} + +function lstatOrNull(path: string) { + try { + return lstatSync(path); + } catch { + return null; + } +} + +/** + * Moves every file under `srcDir` into `dstDir`, creating dirs as needed. + * Conflicts (a path that already exists in `dstDir`) are left behind in + * `srcDir`; empty dirs are pruned as the move drains them. + */ +function moveTreeInto(srcDir: string, dstDir: string): void { + for (const entry of readdirSync(srcDir, { withFileTypes: true })) { + const from = join(srcDir, entry.name); + const to = join(dstDir, entry.name); + if (entry.isDirectory()) { + const toInfo = lstatOrNull(to); + if (!toInfo) { + renameSync(from, to); + continue; + } + if (!toInfo.isDirectory()) continue; + moveTreeInto(from, to); + } else if (!lstatOrNull(to)) { + renameSync(from, to); + } + } + try { + rmdirSync(srcDir); + } catch { + // Conflicts remain — the leftovers stay for the next run to retry. + } +} + +function linkConfigDir(profile: string, main: string, name: string): void { + const src = join(profile, name); + const info = lstatOrNull(src); + if (info?.isSymbolicLink()) return; + if (info && !info.isDirectory()) return; + if (info && readdirSync(src).length > 0) return; + mkdirSync(join(main, name), { recursive: true }); + if (info) rmdirSync(src); + symlinkSync(join(main, name), src); +} + +function linkConfigFile(profile: string, main: string, name: string): void { + const src = join(profile, name); + const target = join(main, name); + const info = lstatOrNull(src); + if (info?.isSymbolicLink()) return; + if (info && (!info.isFile() || info.size > 0)) return; + if (!lstatOrNull(target)) writeFileSync(target, "", { flag: "wx" }); + if (info) unlinkSync(src); + symlinkSync(target, src); +} + +function mergeAndLinkSessionDir( + profile: string, + main: string, + name: string, +): void { + const src = join(profile, name); + const dst = join(main, name); + const pending = `${src}${MERGE_SUFFIX}`; + // Finish what an interrupted earlier merge left behind before anything + // else — src may already be a symlink by now. + if (lstatOrNull(pending)?.isDirectory()) { + mkdirSync(dst, { recursive: true }); + moveTreeInto(pending, dst); + } + const info = lstatOrNull(src); + if (info?.isSymbolicLink()) return; + if (info && !info.isDirectory()) return; + mkdirSync(dst, { recursive: true }); + if (!info) { + symlinkSync(dst, src); + return; + } + // Swap first, merge after: the path is only ever missing for the instant + // between rename and symlink, and open file handles ride the renames. + renameSync(src, pending); + try { + symlinkSync(dst, src); + } catch { + // A live CLI recreated the dir in the gap — restore and skip. + moveTreeInto(pending, src); + return; + } + moveTreeInto(pending, dst); +} + +function mergeAndLinkHistory(profile: string, main: string): void { + const src = join(profile, "history.jsonl"); + const dst = join(main, "history.jsonl"); + const pending = `${src}${MERGE_SUFFIX}`; + if (lstatOrNull(pending)?.isFile()) { + appendFileSync(dst, readFileSync(pending)); + unlinkSync(pending); + } + const info = lstatOrNull(src); + if (info?.isSymbolicLink()) return; + if (info && !info.isFile()) return; + if (!lstatOrNull(dst)) appendFileSync(dst, ""); + if (!info) { + symlinkSync(dst, src); + return; + } + renameSync(src, pending); + try { + symlinkSync(dst, src); + } catch { + renameSync(pending, src); + return; + } + appendFileSync(dst, readFileSync(pending)); + unlinkSync(pending); +} + +function linkSettings(profile: string, main: string): void { + const src = join(profile, "settings.json"); + const dst = join(main, "settings.json"); + const info = lstatOrNull(src); + if (info?.isSymbolicLink()) return; + if (info && !info.isFile()) return; + const dstInfo = lstatOrNull(dst); + if (dstInfo && !dstInfo.isFile()) return; + if (!info && !dstInfo) return; + if (!dstInfo) { + // Main has no settings yet — the profile's become the shared ones. + renameSync(src, dst); + } else if (info) { + renameSync(src, `${src}.pre-share-${Date.now()}`); + } + symlinkSync(dst, src); +} + +/** + * Best-effort per entry: one unmergeable path must not stop the rest, and a + * partially shared profile is strictly better than an unshared one. + */ +export function shareClaudeProfileState( + configDir: string, + mainHome: string = join(homedir(), ".claude"), +): void { + // Windows symlinks need elevation; profiles are a macOS/Linux feature. + if (platform() === "win32") return; + const profile = shareableProfileDir(configDir, mainHome); + if (!profile) return; + const main = resolve(mainHome); + const steps: Array<() => void> = [ + ...CONFIG_DIRS.map((name) => () => linkConfigDir(profile, main, name)), + ...CONFIG_FILES.map((name) => () => linkConfigFile(profile, main, name)), + ...SESSION_DIRS.map( + (name) => () => mergeAndLinkSessionDir(profile, main, name), + ), + () => mergeAndLinkHistory(profile, main), + () => linkSettings(profile, main), + ]; + for (const step of steps) { + try { + step(); + } catch { + // Skipped entry; retried on the next switch to this profile. + } + } +} diff --git a/packages/host-service/src/trpc/router/usage/usage.ts b/packages/host-service/src/trpc/router/usage/usage.ts index 9524b328e99..b13fa3a7701 100644 --- a/packages/host-service/src/trpc/router/usage/usage.ts +++ b/packages/host-service/src/trpc/router/usage/usage.ts @@ -19,6 +19,7 @@ import { } from "./default-account"; import { removeClaudeProfile, removeCodexHome } from "./profile-remove"; import { seedClaudeProfileOnboarding } from "./profile-seed"; +import { shareClaudeProfileState } from "./profile-share"; import { discoverClaudeProfiles, discoverCodexHomes } from "./profiles"; import type { UsageAccount } from "./types"; @@ -149,11 +150,14 @@ export const usageRouter = router({ // Secondary profiles read hooks from their own dir, so agents launched // there would otherwise lose lifecycle/status reporting; fresh Claude // profiles also need onboarding marked done or the first launch runs - // the first-boot wizard. Best-effort: a failed merge must not undo - // the switch. + // the first-boot wizard. Claude profiles first get their shared state + // linked into ~/.claude (one history/settings across accounts — + // profile-share.ts), so the hook merge lands in the shared settings + // file. Best-effort: a failed merge must not undo the switch. if (input.selection !== null) { try { if (input.provider === "claude") { + shareClaudeProfileState(input.selection); ensureClaudeManagedHooksAt(input.selection); seedClaudeProfileOnboarding(input.selection); } else { @@ -214,9 +218,10 @@ export const usageRouter = router({ }), /** - * One-time preparation for a freshly added Claude profile: mark onboarding - * complete so the first agent launch doesn't open the first-boot wizard. - * Only accepts discovered profile dirs. + * One-time preparation for a freshly added Claude profile: link its shared + * state into ~/.claude (profile-share.ts) and mark onboarding complete so + * the first agent launch doesn't open the first-boot wizard. Only accepts + * discovered profile dirs. */ prepareClaudeProfile: protectedProcedure .input(z.object({ configDir: z.string() })) @@ -228,6 +233,7 @@ export const usageRouter = router({ message: `No Claude profile found at ${input.configDir}.`, }); } + shareClaudeProfileState(input.configDir); seedClaudeProfileOnboarding(input.configDir); return { success: true as const }; }), From 62866e70622860516e170176348e6992112b7cd4 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 18 Aug 2026 17:23:47 -0700 Subject: [PATCH 2/7] feat(desktop,host-service): provision account profiles as complete environments with shared history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges the two halves of making a secondary provider account (a CLAUDE_CONFIG_DIR / CODEX_HOME profile dir, #6585) feel like the same install as the default account: Config surfaces (agent-setup/provider-profiles.ts + profile-sharing.ts): capability dirs (skills, plugins, agents, commands, output-styles; prompts for Codex) are symlinked at the default account's so later installs stay shared; files the CLIs write-tmp-then-rename (CLAUDE.md, settings.json, .claude.json, config.toml, AGENTS.md) are copied or key-merged instead — a rename would replace a symlink and silently fork the config. A per-profile ledger (.superset-profile.json) records what was written so a value the user changes inside a profile is never clobbered. Credentials, identity keys, and Superset's hooks stay per-profile; auth-related settings keys and env credentials are filtered from the merge. Session history (host-service usage/profile-share.ts): projects/, sessions/, history.jsonl and the other session-scoped state are shared by symlink — safe there because transcripts are append-only. Existing trees are merged via rename -> symlink -> merge so live sessions ride the renames on the same inodes; conflicts wait in a .superset-merge dir for the next run. Both run together (usage/account-provisioning.ts) when an account is added, when one is selected, and at host boot for the selected accounts. prepareClaudeProfile generalizes to prepareAccount (both providers); profile-seed.ts is absorbed into the .claude.json merge's forced hasCompletedOnboarding. The usage-history scanner realpath- dedupes scan roots so a symlinked projects/ tree is not walked twice. --- .../usage/components/UsageView/UsageView.tsx | 2 +- .../AddAccountDialog/AddAccountDialog.tsx | 20 +- docs/agent-tooling.md | 26 ++ packages/agent-setup/src/index.ts | 5 + packages/agent-setup/src/managed-skills.ts | 43 ++- .../agent-setup/src/profile-sharing.test.ts | 262 +++++++++++++++ packages/agent-setup/src/profile-sharing.ts | 298 ++++++++++++++++++ .../agent-setup/src/provider-profiles.test.ts | 225 +++++++++++++ packages/agent-setup/src/provider-profiles.ts | 272 ++++++++++++++++ packages/host-service/src/app.ts | 7 + .../trpc/router/usage/account-provisioning.ts | 58 ++++ .../trpc/router/usage/profile-remove.test.ts | 36 ++- .../trpc/router/usage/profile-seed.test.ts | 50 --- .../src/trpc/router/usage/profile-seed.ts | 40 --- .../trpc/router/usage/profile-share.test.ts | 65 ++-- .../src/trpc/router/usage/profile-share.ts | 73 +---- .../src/trpc/router/usage/usage.ts | 71 +++-- 17 files changed, 1302 insertions(+), 251 deletions(-) create mode 100644 packages/agent-setup/src/profile-sharing.test.ts create mode 100644 packages/agent-setup/src/profile-sharing.ts create mode 100644 packages/agent-setup/src/provider-profiles.test.ts create mode 100644 packages/agent-setup/src/provider-profiles.ts create mode 100644 packages/host-service/src/trpc/router/usage/account-provisioning.ts delete mode 100644 packages/host-service/src/trpc/router/usage/profile-seed.test.ts delete mode 100644 packages/host-service/src/trpc/router/usage/profile-seed.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx index bf3baa8a472..cabb58a63f5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx @@ -234,7 +234,7 @@ export function UsageView({ hostUrl }: { hostUrl: string | null }) { `New ${PROVIDER_LABELS[account.provider]} terminals and agents will use ${account.email ?? account.sourceLabel}.`, { description: - "Running sessions keep their current account — restart them to switch.", + "Your skills, plugins, MCP servers, and settings come along. Running sessions keep their current account — restart them to switch.", }, ); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx index 22496237690..5ea3acd8948 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx @@ -155,13 +155,14 @@ export function AddAccountDialog({ } if (found) return; - const seedProfile = (configDir: string) => { - // A fresh (or re-signed) Claude profile may have the CLI's first-boot - // wizard armed; mark onboarding done so its first agent launch opens - // the prompt. + const provisionProfile = (selection: string) => { + // Shares the default account's skills, plugins, MCP servers, and + // settings into the new profile dir — a bare login there would boot + // the CLI on an empty install (and, for Claude, into the first-boot + // wizard). if (!hostUrl) return; void getHostServiceClientByUrl(hostUrl) - .usage.prepareClaudeProfile.mutate({ configDir }) + .usage.prepareAccount.mutate({ provider, selection }) .catch(() => {}); }; @@ -174,9 +175,8 @@ export function AddAccountDialog({ if (change) { setFound({ selection: null, label: change.label }); onAccountAdded(); - if (switchTarget.provider === "claude" && switchTarget.selection) { - seedProfile(switchTarget.selection); - } + // The system default (null) is the share's source, not a target. + if (switchTarget.selection) provisionProfile(switchTarget.selection); } return; } @@ -184,7 +184,7 @@ export function AddAccountDialog({ if (fresh) { setFound(fresh); onAccountAdded(); - if (provider === "claude") seedProfile(fresh.selection); + provisionProfile(fresh.selection); } }, [ open, @@ -225,7 +225,7 @@ export function AddAccountDialog({ {switchDescription ?? - "Sign in to a second subscription as a separate profile. Your current login is untouched."} + "Sign in to a second subscription as a separate profile. Your current login is untouched, and the new profile shares your skills, plugins, MCP servers, and settings."} diff --git a/docs/agent-tooling.md b/docs/agent-tooling.md index 5022cacfb0d..2106b770ab1 100644 --- a/docs/agent-tooling.md +++ b/docs/agent-tooling.md @@ -34,6 +34,32 @@ Everything else links to those: Agents other than Claude Code should read the relevant `.agents/skills/*/SKILL.md` when its description matches the task. +## Provider accounts (multi-login) + +The Usage tab can hold several Claude Code / Codex logins and pick which one new terminals and +agents use. A login is just a config dir the CLI is pointed at — `CLAUDE_CONFIG_DIR` / +`CODEX_HOME`, injected at PTY and agent launch by +`packages/host-service/src/trpc/router/usage/default-account.ts`. Superset never touches the +credential stores; the provider CLIs own every login end to end. + +Because those CLIs read *everything* from their active config dir, a second dir would otherwise +mean a second, empty setup. `packages/agent-setup/src/provider-profiles.ts` provisions each +non-default profile from the default account (`~/.claude`, `~/.codex`): + +- Capability directories (`skills/`, `plugins/`, `agents/`, `commands/`, `output-styles/`; + `prompts/` for Codex) are **symlinked** at the default account's, so anything installed later is + shared with no sync step. +- Files (`CLAUDE.md`, `config.toml`, `AGENTS.md`) are **copied**, and `settings.json` / + `.claude.json` are **key-merged** — the CLIs rename-replace files, which would silently break a + file symlink. +- Copies and merged keys are recorded in `/.superset-profile.json`, so anything the user + changes inside a profile is never overwritten by a later provision. +- Per-account and never shared: credentials, `oauthAccount`/`userID`, transcripts and session + state, and Superset's own lifecycle hooks (written per profile). + +Provisioning is idempotent and runs when an account is added, when one is selected, and at host +boot for the selected accounts (`usage/account-provisioning.ts`). + ## Testing the CLI and skills against the dev app The dev desktop app is local-first: it registers its host service under a diff --git a/packages/agent-setup/src/index.ts b/packages/agent-setup/src/index.ts index b3c1d0ac48a..2dbbee19a2b 100644 --- a/packages/agent-setup/src/index.ts +++ b/packages/agent-setup/src/index.ts @@ -60,6 +60,11 @@ export { ensureClaudeManagedHooksAt, ensureCodexManagedHooksAt, } from "./agent-wrappers-claude-codex-opencode"; +export { + type ProfileProvisionReport, + provisionClaudeProfile, + provisionCodexProfile, +} from "./provider-profiles"; export { getCommandShellArgs, getShellArgs, getShellEnv }; diff --git a/packages/agent-setup/src/managed-skills.ts b/packages/agent-setup/src/managed-skills.ts index 54a1ab6105a..7e6f5801bbd 100644 --- a/packages/agent-setup/src/managed-skills.ts +++ b/packages/agent-setup/src/managed-skills.ts @@ -120,17 +120,17 @@ async function syncDir(src: string, dest: string): Promise { } } -/** Provisions the bundled plugin as a Claude Code skills-directory plugin. */ +/** + * Provisions the bundled plugin as a Claude Code skills-directory plugin + * inside one Claude config dir — `~/.claude` for the default account, or a + * profile dir whose own `skills/` the user owns (a shared profile links + * `skills/` at the default account instead, and gets it that way). + */ async function provisionClaudePlugin( bundledPluginDir: string, - homeDir: string, + claudeDir: string, ): Promise { - const target = path.join( - homeDir, - ".claude", - "skills", - CLAUDE_PLUGIN_DIR_NAME, - ); + const target = path.join(claudeDir, "skills", CLAUDE_PLUGIN_DIR_NAME); const sentinel = path.join(target, MANAGED_SENTINEL_NAME); if (fs.existsSync(target) && !fs.existsSync(sentinel)) { console.log(`[agent-setup] Skipping user-owned plugin dir at ${target}`); @@ -245,7 +245,10 @@ export async function createManagedSkills( ); try { - await provisionClaudePlugin(bundledPluginDir, homeDir); + await provisionClaudePlugin( + bundledPluginDir, + path.join(homeDir, ".claude"), + ); } catch (error) { console.warn("[agent-setup] Failed to provision Claude plugin:", error); } @@ -331,3 +334,25 @@ export async function createManagedSkills( console.log("[agent-setup] Managed skills provisioned"); } + +/** + * Provisions the bundled Superset plugin into one Claude config dir. Used for + * a secondary account whose `skills/` directory the user owns, so it can't be + * linked at the default account's — the plugin is written into it directly + * instead of being shared. + */ +export async function provisionManagedClaudePluginAt( + claudeDir: string, + options: ManagedSkillsOptions = {}, +): Promise { + const bundledPluginDir = options.templatesDir + ? path.join(options.templatesDir, "plugin") + : getBundledPluginDir(); + if (!fs.existsSync(path.join(bundledPluginDir, "skills"))) { + console.warn( + `[agent-setup] Bundled plugin missing at ${bundledPluginDir}; skipping plugin provisioning for ${claudeDir}`, + ); + return; + } + await provisionClaudePlugin(bundledPluginDir, claudeDir); +} diff --git a/packages/agent-setup/src/profile-sharing.test.ts b/packages/agent-setup/src/profile-sharing.test.ts new file mode 100644 index 00000000000..afe469c7291 --- /dev/null +++ b/packages/agent-setup/src/profile-sharing.test.ts @@ -0,0 +1,262 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readlinkSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { + linkSharedDir, + mergeSharedJsonKeys, + type ProfileLedger, + readProfileLedger, + syncSharedFile, + writeProfileLedger, +} from "./profile-sharing"; + +const TEST_ROOT = path.join( + os.tmpdir(), + `superset-profile-sharing-${process.pid}-${Date.now()}`, +); +const SOURCE = path.join(TEST_ROOT, "default"); +const PROFILE = path.join(TEST_ROOT, "profile"); + +function ledger(): ProfileLedger { + return readProfileLedger(PROFILE, SOURCE); +} + +beforeEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); + mkdirSync(SOURCE, { recursive: true }); + mkdirSync(PROFILE, { recursive: true }); +}); + +afterEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); +}); + +describe("linkSharedDir", () => { + it("links a free path and is idempotent", () => { + mkdirSync(path.join(SOURCE, "skills", "demo"), { recursive: true }); + writeFileSync(path.join(SOURCE, "skills", "demo", "SKILL.md"), "hi"); + + const target = path.join(PROFILE, "skills"); + expect(linkSharedDir(path.join(SOURCE, "skills"), target)).toBe("linked"); + expect(lstatSync(target).isSymbolicLink()).toBe(true); + expect(readFileSync(path.join(target, "demo", "SKILL.md"), "utf-8")).toBe( + "hi", + ); + expect(linkSharedDir(path.join(SOURCE, "skills"), target)).toBe( + "unchanged", + ); + }); + + it("shares later additions without re-provisioning", () => { + mkdirSync(path.join(SOURCE, "skills"), { recursive: true }); + linkSharedDir(path.join(SOURCE, "skills"), path.join(PROFILE, "skills")); + + mkdirSync(path.join(SOURCE, "skills", "added"), { recursive: true }); + expect(existsSync(path.join(PROFILE, "skills", "added"))).toBe(true); + }); + + it("reports absent when the default account has no such dir", () => { + expect( + linkSharedDir( + path.join(SOURCE, "plugins"), + path.join(PROFILE, "plugins"), + ), + ).toBe("absent"); + expect(existsSync(path.join(PROFILE, "plugins"))).toBe(false); + }); + + it("adopts an empty dir but never one with content", () => { + mkdirSync(path.join(SOURCE, "skills"), { recursive: true }); + mkdirSync(path.join(PROFILE, "skills"), { recursive: true }); + expect( + linkSharedDir(path.join(SOURCE, "skills"), path.join(PROFILE, "skills")), + ).toBe("linked"); + + rmSync(path.join(PROFILE, "skills"), { recursive: true, force: true }); + mkdirSync(path.join(PROFILE, "skills", "mine"), { recursive: true }); + expect( + linkSharedDir(path.join(SOURCE, "skills"), path.join(PROFILE, "skills")), + ).toBe("user-owned"); + expect(lstatSync(path.join(PROFILE, "skills")).isDirectory()).toBe(true); + expect(lstatSync(path.join(PROFILE, "skills")).isSymbolicLink()).toBe( + false, + ); + }); + + it("leaves a link the user aimed elsewhere, replaces a dangling one", () => { + mkdirSync(path.join(SOURCE, "skills"), { recursive: true }); + const elsewhere = path.join(TEST_ROOT, "elsewhere"); + mkdirSync(elsewhere, { recursive: true }); + symlinkSync(elsewhere, path.join(PROFILE, "skills"), "dir"); + expect( + linkSharedDir(path.join(SOURCE, "skills"), path.join(PROFILE, "skills")), + ).toBe("user-owned"); + expect(readlinkSync(path.join(PROFILE, "skills"))).toBe(elsewhere); + + rmSync(elsewhere, { recursive: true, force: true }); + expect( + linkSharedDir(path.join(SOURCE, "skills"), path.join(PROFILE, "skills")), + ).toBe("linked"); + }); +}); + +describe("syncSharedFile", () => { + const args = () => ({ + sourcePath: path.join(SOURCE, "CLAUDE.md"), + targetPath: path.join(PROFILE, "CLAUDE.md"), + surface: "CLAUDE.md", + ledger: ledger(), + }); + + it("copies, then reports unchanged", () => { + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "be brief"); + const first = args(); + expect(syncSharedFile(first)).toBe("synced"); + expect(readFileSync(path.join(PROFILE, "CLAUDE.md"), "utf-8")).toBe( + "be brief", + ); + expect(syncSharedFile({ ...first, ledger: first.ledger })).toBe( + "unchanged", + ); + }); + + it("re-syncs its own copy when the default account changes", () => { + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "v1"); + const state = args(); + syncSharedFile(state); + writeProfileLedger(PROFILE, state.ledger); + + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "v2"); + expect(syncSharedFile(args())).toBe("synced"); + expect(readFileSync(path.join(PROFILE, "CLAUDE.md"), "utf-8")).toBe("v2"); + }); + + it("never clobbers an edit made inside the profile", () => { + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "v1"); + const state = args(); + syncSharedFile(state); + writeProfileLedger(PROFILE, state.ledger); + + writeFileSync(path.join(PROFILE, "CLAUDE.md"), "mine"); + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "v2"); + expect(syncSharedFile(args())).toBe("user-owned"); + expect(readFileSync(path.join(PROFILE, "CLAUDE.md"), "utf-8")).toBe("mine"); + }); + + it("leaves a pre-existing file the profile already had", () => { + writeFileSync(path.join(SOURCE, "CLAUDE.md"), "shared"); + writeFileSync(path.join(PROFILE, "CLAUDE.md"), "theirs"); + expect(syncSharedFile(args())).toBe("user-owned"); + }); +}); + +describe("mergeSharedJsonKeys", () => { + const settings = () => ({ + sourcePath: path.join(SOURCE, "settings.json"), + targetPath: path.join(PROFILE, "settings.json"), + surface: "settings.json", + ledger: ledger(), + pick: (source: Record) => source, + }); + + function readTarget(): Record { + return JSON.parse( + readFileSync(path.join(PROFILE, "settings.json"), "utf-8"), + ); + } + + it("copies shared keys and preserves keys the profile owns", () => { + writeFileSync( + path.join(SOURCE, "settings.json"), + JSON.stringify({ model: "opus", effortLevel: "high" }), + ); + writeFileSync( + path.join(PROFILE, "settings.json"), + JSON.stringify({ hooks: { Stop: [] } }), + ); + + const state = settings(); + expect(mergeSharedJsonKeys(state)).toBe("merged"); + writeProfileLedger(PROFILE, state.ledger); + expect(readTarget()).toEqual({ + hooks: { Stop: [] }, + model: "opus", + effortLevel: "high", + }); + expect(mergeSharedJsonKeys(settings())).toBe("unchanged"); + }); + + it("keeps a value the user changed inside the profile", () => { + writeFileSync( + path.join(SOURCE, "settings.json"), + JSON.stringify({ model: "opus" }), + ); + const state = settings(); + mergeSharedJsonKeys(state); + writeProfileLedger(PROFILE, state.ledger); + + writeFileSync( + path.join(PROFILE, "settings.json"), + JSON.stringify({ model: "sonnet" }), + ); + writeFileSync( + path.join(SOURCE, "settings.json"), + JSON.stringify({ model: "fable" }), + ); + expect(mergeSharedJsonKeys(settings())).toBe("unchanged"); + expect(readTarget().model).toBe("sonnet"); + }); + + it("skips a state file the profile does not have yet", () => { + writeFileSync( + path.join(SOURCE, "settings.json"), + JSON.stringify({ mcpServers: { linear: {} } }), + ); + expect( + mergeSharedJsonKeys({ + ...settings(), + force: { hasCompletedOnboarding: true }, + requireExistingTarget: true, + }), + ).toBe("absent"); + expect(existsSync(path.join(PROFILE, "settings.json"))).toBe(false); + }); + + it("re-applies forced values even when they were changed", () => { + writeFileSync(path.join(SOURCE, "settings.json"), JSON.stringify({})); + writeFileSync( + path.join(PROFILE, "settings.json"), + JSON.stringify({ hasCompletedOnboarding: false }), + ); + expect( + mergeSharedJsonKeys({ + ...settings(), + force: { hasCompletedOnboarding: true }, + requireExistingTarget: true, + }), + ).toBe("merged"); + expect(readTarget().hasCompletedOnboarding).toBe(true); + }); + + it("leaves an unparsable profile file alone", () => { + writeFileSync( + path.join(SOURCE, "settings.json"), + JSON.stringify({ model: "opus" }), + ); + writeFileSync(path.join(PROFILE, "settings.json"), "{ not json"); + expect(mergeSharedJsonKeys(settings())).toBe("user-owned"); + expect(readFileSync(path.join(PROFILE, "settings.json"), "utf-8")).toBe( + "{ not json", + ); + }); +}); diff --git a/packages/agent-setup/src/profile-sharing.ts b/packages/agent-setup/src/profile-sharing.ts new file mode 100644 index 00000000000..1c83248aac6 --- /dev/null +++ b/packages/agent-setup/src/profile-sharing.ts @@ -0,0 +1,298 @@ +/** + * Shared-config plumbing behind provider "accounts". + * + * A secondary account is just a config dir the provider CLI gets pointed at + * (CLAUDE_CONFIG_DIR / CODEX_HOME), so switching to one handed the CLI a + * blank install: no skills, no plugins, no MCP servers, none of the user's + * settings. These primitives make a profile dir mirror the default account + * for everything that isn't a login. + * + * Two mechanisms, picked by what the CLI does to the path: + * + * - Directories are symlinked at the default account's dir. The share stays + * live — a skill or plugin installed later shows up in every account with + * no sync step — and a directory can't be atomically replaced out from + * under the link. + * - Files are copied, and JSON files key-merged, because the CLIs write them + * with write-tmp-then-rename: a symlink would be replaced by a regular + * file on the first write and the config would silently fork. + * + * Copies and merges record what they wrote in a per-profile ledger, so a + * value the user changed inside the profile is never clobbered by the next + * provision. Nothing here reads or writes credentials. + */ + +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { writeFileIfChanged } from "./agent-wrappers-common"; + +/** Per-profile record of what we last wrote, kept inside the profile dir. */ +export const PROFILE_LEDGER_NAME = ".superset-profile.json"; + +export type SurfaceOutcome = + /** A symlink now points the profile at the default account's dir. */ + | "linked" + /** Content copied from the default account. */ + | "synced" + /** JSON keys copied from the default account. */ + | "merged" + /** Already matched the default account. */ + | "unchanged" + /** The profile has its own version — left untouched. */ + | "user-owned" + /** Nothing to share: the default account has no such surface. */ + | "absent"; + +export interface ProfileLedger { + version: 1; + /** Default-account dir these values were shared from. */ + sharedFrom: string; + /** Surface name -> sha256 of the content we last wrote. */ + files: Record; + /** Surface name -> key -> serialized value we last wrote. */ + keys: Record>; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex"); +} + +function readJsonObject(filePath: string): Record | null { + try { + const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8")); + return isPlainObject(parsed) ? parsed : null; + } catch { + return null; + } +} + +export function readProfileLedger( + profileDir: string, + sharedFrom: string, +): ProfileLedger { + const empty: ProfileLedger = { + version: 1, + sharedFrom, + files: {}, + keys: {}, + }; + const stored = readJsonObject(path.join(profileDir, PROFILE_LEDGER_NAME)); + if (!stored) return empty; + return { + version: 1, + sharedFrom, + files: isPlainObject(stored.files) + ? (stored.files as Record) + : {}, + keys: isPlainObject(stored.keys) + ? (stored.keys as Record>) + : {}, + }; +} + +export function writeProfileLedger( + profileDir: string, + ledger: ProfileLedger, +): void { + try { + writeFileIfChanged( + path.join(profileDir, PROFILE_LEDGER_NAME), + JSON.stringify(ledger, null, 2), + 0o644, + ); + } catch (error) { + console.warn( + `[agent-setup] Could not write the profile ledger in ${profileDir}:`, + error, + ); + } +} + +function lstatOrNull(target: string): fs.Stats | null { + try { + return fs.lstatSync(target); + } catch { + return null; + } +} + +function realpathOrNull(target: string): string | null { + try { + return fs.realpathSync(target); + } catch { + return null; + } +} + +/** + * Points `targetPath` at `sourceDir` with a symlink. Only ever claims a path + * that is free: an existing real directory with anything in it, or a symlink + * the user aimed somewhere else, is reported as user-owned and left alone. + */ +export function linkSharedDir( + sourceDir: string, + targetPath: string, +): SurfaceOutcome { + const source = realpathOrNull(sourceDir); + if (source === null) return "absent"; + + const existing = lstatOrNull(targetPath); + if (existing?.isSymbolicLink()) { + const current = realpathOrNull(targetPath); + if (current === source) return "unchanged"; + // A link the user aimed elsewhere is deliberate; a dangling one is + // debris (a profile whose old share target was deleted) and ours to + // replace. + if (current !== null) return "user-owned"; + fs.unlinkSync(targetPath); + } else if (existing) { + if (!existing.isDirectory()) return "user-owned"; + if (fs.readdirSync(targetPath).length > 0) return "user-owned"; + fs.rmdirSync(targetPath); + } + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + // "junction" is the one dir link Windows grants without elevation. + fs.symlinkSync( + source, + targetPath, + process.platform === "win32" ? "junction" : "dir", + ); + return "linked"; +} + +export interface SyncSharedFileArgs { + sourcePath: string; + targetPath: string; + /** Ledger key, normally the file name. */ + surface: string; + ledger: ProfileLedger; +} + +/** + * Copies a file from the default account into the profile. The profile's copy + * is only overwritten while it still holds what we last put there — once the + * user edits it inside the profile, that edit wins from then on. + */ +export function syncSharedFile({ + sourcePath, + targetPath, + surface, + ledger, +}: SyncSharedFileArgs): SurfaceOutcome { + let content: string; + try { + content = fs.readFileSync(sourcePath, "utf-8"); + } catch { + return "absent"; + } + const hash = sha256(content); + + if (fs.existsSync(targetPath)) { + let current: string; + try { + current = fs.readFileSync(targetPath, "utf-8"); + } catch { + return "user-owned"; + } + const currentHash = sha256(current); + if (currentHash === hash) { + ledger.files[surface] = hash; + return "unchanged"; + } + if (ledger.files[surface] !== currentHash) return "user-owned"; + } + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + writeFileIfChanged(targetPath, content, 0o644); + ledger.files[surface] = hash; + return "synced"; +} + +export interface MergeSharedJsonKeysArgs { + sourcePath: string; + targetPath: string; + /** Ledger key, normally the file name. */ + surface: string; + ledger: ProfileLedger; + /** Shared subset of the default account's file. */ + pick(source: Record): Record; + /** + * Values written regardless of the default account — facts about being a + * secondary profile rather than shared config, so they are not tracked in + * the ledger and are re-applied whenever they drift. + */ + force?: Record; + /** + * Skip entirely when the profile has no readable file yet. Set for state + * files the CLI owns end to end (`.claude.json`), where creating one + * ourselves would fabricate a login that has not happened. + */ + requireExistingTarget?: boolean; +} + +/** + * Copies selected top-level keys from the default account's JSON file into + * the profile's. Per key, the same rule as syncSharedFile: we only overwrite + * a value that is still the one we wrote. A key we wrote and the user then + * deleted comes back — the surface is shared, and re-adding it is the whole + * point of the share. + */ +export function mergeSharedJsonKeys({ + sourcePath, + targetPath, + surface, + ledger, + pick, + force, + requireExistingTarget, +}: MergeSharedJsonKeysArgs): SurfaceOutcome { + const existing = readJsonObject(targetPath); + if (requireExistingTarget && existing === null) return "absent"; + if (existing === null && fs.existsSync(targetPath)) { + console.warn( + `[agent-setup] Could not parse ${targetPath}; skipping the shared-config merge`, + ); + return "user-owned"; + } + + const source = readJsonObject(sourcePath); + const desired = source ? pick(source) : {}; + if (source === null && !force) return "absent"; + + const target = existing ?? {}; + const written = ledger.keys[surface] ?? {}; + let changed = false; + + for (const [key, value] of Object.entries(desired)) { + const serialized = JSON.stringify(value); + const current = key in target ? JSON.stringify(target[key]) : undefined; + if (current === serialized) { + written[key] = serialized; + continue; + } + // Present with a value we never wrote: the user set it in this profile. + if (current !== undefined && written[key] !== current) continue; + target[key] = value; + written[key] = serialized; + changed = true; + } + + for (const [key, value] of Object.entries(force ?? {})) { + if (JSON.stringify(target[key]) === JSON.stringify(value)) continue; + target[key] = value; + changed = true; + } + + ledger.keys[surface] = written; + if (!changed) return "unchanged"; + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + writeFileIfChanged(targetPath, JSON.stringify(target, null, 2), 0o644); + return "merged"; +} diff --git a/packages/agent-setup/src/provider-profiles.test.ts b/packages/agent-setup/src/provider-profiles.test.ts new file mode 100644 index 00000000000..c5137b8d8f4 --- /dev/null +++ b/packages/agent-setup/src/provider-profiles.test.ts @@ -0,0 +1,225 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { + provisionClaudeProfile, + provisionCodexProfile, +} from "./provider-profiles"; + +const TEST_ROOT = path.join( + os.tmpdir(), + `superset-provider-profiles-${process.pid}-${Date.now()}`, +); +const HOME = path.join(TEST_ROOT, "home"); +const DEFAULT_DIR = path.join(HOME, ".claude"); +const PROFILE = path.join(HOME, ".claude-work"); + +function writeJson(filePath: string, value: unknown): void { + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, JSON.stringify(value, null, 2)); +} + +function readJson(filePath: string): Record { + return JSON.parse(readFileSync(filePath, "utf-8")); +} + +/** A default account with something to share on every surface. */ +function seedDefaultAccount(): void { + mkdirSync(path.join(DEFAULT_DIR, "skills", "redesign"), { recursive: true }); + writeFileSync( + path.join(DEFAULT_DIR, "skills", "redesign", "SKILL.md"), + "# redesign", + ); + mkdirSync(path.join(DEFAULT_DIR, "plugins"), { recursive: true }); + writeJson(path.join(DEFAULT_DIR, "plugins", "installed_plugins.json"), { + version: 2, + }); + mkdirSync(path.join(DEFAULT_DIR, "agents"), { recursive: true }); + writeFileSync(path.join(DEFAULT_DIR, "CLAUDE.md"), "be brief"); + writeJson(path.join(DEFAULT_DIR, "settings.json"), { + model: "claude-opus-5", + effortLevel: "high", + enabledPlugins: { "superset@superset": true }, + env: { DISABLE_TELEMETRY: "1", ANTHROPIC_API_KEY: "sk-secret" }, + apiKeyHelper: "/usr/local/bin/key.sh", + hooks: { Stop: [{ hooks: [{ type: "command", command: "user-hook" }] }] }, + }); + writeJson(path.join(HOME, ".claude.json"), { + mcpServers: { linear: { command: "linear-mcp" } }, + theme: "dark", + oauthAccount: { emailAddress: "default@example.com" }, + userID: "default-user", + }); +} + +/** A profile that has completed its own login. */ +function seedProfile(): void { + writeJson(path.join(PROFILE, ".claude.json"), { + oauthAccount: { emailAddress: "work@example.com" }, + userID: "work-user", + }); + writeFileSync(path.join(PROFILE, ".credentials.json"), "{}"); +} + +beforeEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); + mkdirSync(HOME, { recursive: true }); +}); + +afterEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); +}); + +describe("provisionClaudeProfile", () => { + it("shares every capability surface into the profile", async () => { + seedDefaultAccount(); + seedProfile(); + + const report = await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + expect(report.surfaces["skills/"]).toBe("linked"); + expect(report.surfaces["plugins/"]).toBe("linked"); + expect(report.surfaces["agents/"]).toBe("linked"); + // The default account has none of these; nothing to share. + expect(report.surfaces["commands/"]).toBe("absent"); + expect(lstatSync(path.join(PROFILE, "skills")).isSymbolicLink()).toBe(true); + expect( + readFileSync( + path.join(PROFILE, "skills", "redesign", "SKILL.md"), + "utf-8", + ), + ).toBe("# redesign"); + expect(readFileSync(path.join(PROFILE, "CLAUDE.md"), "utf-8")).toBe( + "be brief", + ); + }); + + it("shares settings but never the login's own resolution", async () => { + seedDefaultAccount(); + seedProfile(); + + await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + const settings = readJson(path.join(PROFILE, "settings.json")); + expect(settings.model).toBe("claude-opus-5"); + expect(settings.effortLevel).toBe("high"); + expect(settings.enabledPlugins).toEqual({ "superset@superset": true }); + expect(settings.env).toEqual({ DISABLE_TELEMETRY: "1" }); + expect(settings.apiKeyHelper).toBeUndefined(); + // Hooks are provisioned per profile, not copied from the default's file. + const hooks = settings.hooks as Record; + expect(JSON.stringify(hooks)).not.toContain("user-hook"); + expect(JSON.stringify(hooks)).toContain("notify.sh"); + }); + + it("shares MCP servers and completes onboarding without touching identity", async () => { + seedDefaultAccount(); + seedProfile(); + + await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + const state = readJson(path.join(PROFILE, ".claude.json")); + expect(state.mcpServers).toEqual({ linear: { command: "linear-mcp" } }); + expect(state.theme).toBe("dark"); + expect(state.hasCompletedOnboarding).toBe(true); + expect(state.oauthAccount).toEqual({ emailAddress: "work@example.com" }); + expect(state.userID).toBe("work-user"); + // Credentials are the one thing an account owns outright. + expect(readFileSync(path.join(PROFILE, ".credentials.json"), "utf-8")).toBe( + "{}", + ); + expect(existsSync(path.join(DEFAULT_DIR, ".credentials.json"))).toBe(false); + }); + + it("skips the state merge until the profile has logged in", async () => { + seedDefaultAccount(); + mkdirSync(PROFILE, { recursive: true }); + + const report = await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + expect(report.surfaces[".claude.json"]).toBe("absent"); + expect(existsSync(path.join(PROFILE, ".claude.json"))).toBe(false); + }); + + it("is idempotent", async () => { + seedDefaultAccount(); + seedProfile(); + + await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + const second = await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + expect(second.surfaces["skills/"]).toBe("unchanged"); + expect(second.surfaces["CLAUDE.md"]).toBe("unchanged"); + expect(second.surfaces["settings.json"]).toBe("unchanged"); + expect(second.surfaces[".claude.json"]).toBe("unchanged"); + }); + + it("writes the bundled plugin into a skills dir the profile owns", async () => { + seedDefaultAccount(); + seedProfile(); + mkdirSync(path.join(PROFILE, "skills", "mine"), { recursive: true }); + writeFileSync(path.join(PROFILE, "skills", "mine", "SKILL.md"), "# mine"); + + const report = await provisionClaudeProfile(PROFILE, { homeDir: HOME }); + + expect(report.surfaces["skills/"]).toBe("user-owned"); + expect(existsSync(path.join(PROFILE, "skills", "mine", "SKILL.md"))).toBe( + true, + ); + expect(existsSync(path.join(PROFILE, "skills", "superset", "skills"))).toBe( + true, + ); + }); + + it("does nothing to the default account itself", async () => { + seedDefaultAccount(); + + const report = await provisionClaudeProfile(DEFAULT_DIR, { homeDir: HOME }); + + expect(report.surfaces).toEqual({}); + expect(existsSync(path.join(DEFAULT_DIR, ".superset-profile.json"))).toBe( + false, + ); + }); +}); + +describe("provisionCodexProfile", () => { + const DEFAULT_CODEX = path.join(HOME, ".codex"); + const CODEX_PROFILE = path.join(HOME, ".codex-work"); + + it("shares prompts, config, and instructions but not auth", async () => { + mkdirSync(path.join(DEFAULT_CODEX, "prompts"), { recursive: true }); + writeFileSync(path.join(DEFAULT_CODEX, "prompts", "review.md"), "review"); + writeFileSync( + path.join(DEFAULT_CODEX, "config.toml"), + 'model = "gpt-5"\n[mcp_servers.linear]\ncommand = "linear-mcp"\n', + ); + writeFileSync(path.join(DEFAULT_CODEX, "AGENTS.md"), "be brief"); + writeFileSync(path.join(DEFAULT_CODEX, "auth.json"), '{"tokens":{}}'); + mkdirSync(CODEX_PROFILE, { recursive: true }); + + const report = await provisionCodexProfile(CODEX_PROFILE, { + homeDir: HOME, + }); + + expect(report.surfaces["prompts/"]).toBe("linked"); + expect( + readFileSync(path.join(CODEX_PROFILE, "config.toml"), "utf-8"), + ).toContain("linear-mcp"); + expect(readFileSync(path.join(CODEX_PROFILE, "AGENTS.md"), "utf-8")).toBe( + "be brief", + ); + expect(existsSync(path.join(CODEX_PROFILE, "auth.json"))).toBe(false); + expect( + readFileSync(path.join(CODEX_PROFILE, "hooks.json"), "utf-8"), + ).toContain("notify.sh"); + }); +}); diff --git a/packages/agent-setup/src/provider-profiles.ts b/packages/agent-setup/src/provider-profiles.ts new file mode 100644 index 00000000000..e31a2f7a8f1 --- /dev/null +++ b/packages/agent-setup/src/provider-profiles.ts @@ -0,0 +1,272 @@ +/** + * Makes a provider account (a CLAUDE_CONFIG_DIR / CODEX_HOME profile dir) a + * complete environment rather than a bare login. + * + * The CLIs read *everything* — skills, plugins, subagents, commands, MCP + * servers, settings — from their active config dir, so pointing one at a + * second dir to switch accounts also swapped the user's whole setup for an + * empty one. Provisioning shares each of those surfaces from the default + * account (see profile-sharing.ts for the link/sync/merge rules) and leaves + * exactly one thing per-account: the login, plus the transcript and session + * state that belongs to it. + * + * Idempotent and safe to re-run: it is called when an account is added, when + * one is selected, and for the selected accounts at host boot. + */ + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { + ensureClaudeManagedHooksAt, + ensureCodexManagedHooksAt, +} from "./agent-wrappers-claude-codex-opencode"; +import { provisionManagedClaudePluginAt } from "./managed-skills"; +import { + linkSharedDir, + mergeSharedJsonKeys, + readProfileLedger, + type SurfaceOutcome, + syncSharedFile, + writeProfileLedger, +} from "./profile-sharing"; + +export interface ProfileProvisionReport { + configDir: string; + /** Surface name -> what provisioning did with it, for setup logs. */ + surfaces: Record; +} + +export interface ProvisionProfileOptions { + /** Overridden in tests; the real home in every other caller. */ + homeDir?: string; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function logReport(provider: string, report: ProfileProvisionReport): void { + const summary = Object.entries(report.surfaces) + .map(([surface, outcome]) => `${surface}=${outcome}`) + .join(" "); + console.log( + `[agent-setup] Provisioned ${provider} account ${report.configDir}: ${summary}`, + ); +} + +// --------------------------------------------------------------------------- +// Claude Code +// --------------------------------------------------------------------------- + +/** + * Directories shared by symlink. Everything Claude Code loads as user-level + * capability lives in one of these; what's left in a config dir is the login + * and its own history (projects/, sessions/, shell-snapshots/, …). + */ +const CLAUDE_SHARED_DIRS = [ + "skills", + "plugins", + "agents", + "commands", + "output-styles", +] as const; + +/** Files copied verbatim (the CLI rename-replaces them, so no symlink). */ +const CLAUDE_SHARED_FILES = ["CLAUDE.md"] as const; + +/** + * settings.json keys that must NOT follow the account. Everything else is + * shared by default so a key added by a future CLI release comes along + * without a code change here. + */ +const CLAUDE_PRIVATE_SETTINGS_KEYS = new Set([ + // Provisioned per profile right after the merge — the hooks are identical, + // but they are ours to write, not the user's to share. + "hooks", + // Auth resolution: sharing these would let one account's credential + // mechanism silently answer for another. + "apiKeyHelper", + "awsAuthRefresh", + "awsCredentialExport", + "forceLoginMethod", + "forceLoginOrgUUID", +]); + +/** `env` entries holding a credential; the rest of `env` is shared. */ +const CLAUDE_PRIVATE_ENV_KEYS = new Set([ + "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + "CLAUDE_CODE_OAUTH_TOKEN", +]); + +/** + * `.claude.json` keys that are shared — an allowlist, inverted from + * settings.json on purpose: that file is mostly identity (oauthAccount, + * userID), per-project transcript history, and server-driven caches, none of + * which may cross accounts. + */ +const CLAUDE_SHARED_STATE_KEYS = ["mcpServers", "theme"] as const; + +function pickClaudeSettings( + source: Record, +): Record { + const shared: Record = {}; + for (const [key, value] of Object.entries(source)) { + if (CLAUDE_PRIVATE_SETTINGS_KEYS.has(key)) continue; + if (key === "env" && isPlainObject(value)) { + shared.env = Object.fromEntries( + Object.entries(value).filter( + ([name]) => !CLAUDE_PRIVATE_ENV_KEYS.has(name), + ), + ); + continue; + } + shared[key] = value; + } + return shared; +} + +function pickClaudeState( + source: Record, +): Record { + const shared: Record = {}; + for (const key of CLAUDE_SHARED_STATE_KEYS) { + if (key in source) shared[key] = source[key]; + } + return shared; +} + +/** + * Brings one Claude config dir up to date with the default account. A no-op + * for `~/.claude` itself, which is the source of every share. + */ +export async function provisionClaudeProfile( + configDir: string, + options: ProvisionProfileOptions = {}, +): Promise { + const homeDir = options.homeDir ?? os.homedir(); + const defaultDir = path.join(homeDir, ".claude"); + const target = path.resolve(configDir); + const surfaces: Record = {}; + if (target === path.resolve(defaultDir)) { + return { configDir: target, surfaces }; + } + + fs.mkdirSync(target, { recursive: true }); + const ledger = readProfileLedger(target, defaultDir); + + for (const name of CLAUDE_SHARED_DIRS) { + surfaces[`${name}/`] = linkSharedDir( + path.join(defaultDir, name), + path.join(target, name), + ); + } + for (const name of CLAUDE_SHARED_FILES) { + surfaces[name] = syncSharedFile({ + sourcePath: path.join(defaultDir, name), + targetPath: path.join(target, name), + surface: name, + ledger, + }); + } + + surfaces["settings.json"] = mergeSharedJsonKeys({ + sourcePath: path.join(defaultDir, "settings.json"), + targetPath: path.join(target, "settings.json"), + surface: "settings.json", + ledger, + pick: pickClaudeSettings, + }); + + // Only the default account keeps its state next door at `~/.claude.json`; + // a custom config dir keeps it inside. + surfaces[".claude.json"] = mergeSharedJsonKeys({ + sourcePath: path.join(homeDir, ".claude.json"), + targetPath: path.join(target, ".claude.json"), + surface: ".claude.json", + ledger, + pick: pickClaudeState, + // Without this the profile's first launch opens the first-boot wizard, + // where a stray Enter starts a login that silently rebinds the profile. + force: { hasCompletedOnboarding: true }, + // No parsable state file means no completed login yet — writing one + // would fabricate an account that doesn't exist. + requireExistingTarget: true, + }); + + writeProfileLedger(target, ledger); + // After the settings merge: the merge only writes shared keys, and the + // hook entries have to survive it. + ensureClaudeManagedHooksAt(target); + + if (surfaces["skills/"] === "user-owned") { + // The profile brought its own skills dir, so it isn't sharing ours — + // the bundled Superset plugin has to be written into it directly. + await provisionManagedClaudePluginAt(target); + surfaces["skills/superset"] = "synced"; + } + + const report = { configDir: target, surfaces }; + logReport("Claude", report); + return report; +} + +// --------------------------------------------------------------------------- +// Codex +// --------------------------------------------------------------------------- + +/** `prompts/` holds custom slash-command prompts; the rest of a Codex home + * is auth, history, and rollouts. */ +const CODEX_SHARED_DIRS = ["prompts"] as const; + +/** + * `config.toml` carries the model, approval policy, and MCP servers; + * `AGENTS.md` the global instructions. Neither holds credentials — Codex + * keeps those in `auth.json`, which stays per-account. + */ +const CODEX_SHARED_FILES = ["config.toml", "AGENTS.md"] as const; + +function defaultCodexHome(homeDir: string): string { + const fromEnv = process.env.CODEX_HOME?.trim(); + return fromEnv ? path.resolve(fromEnv) : path.join(homeDir, ".codex"); +} + +/** Brings one Codex home up to date with the default one. */ +export async function provisionCodexProfile( + home: string, + options: ProvisionProfileOptions = {}, +): Promise { + const homeDir = options.homeDir ?? os.homedir(); + const defaultDir = defaultCodexHome(homeDir); + const target = path.resolve(home); + const surfaces: Record = {}; + if (target === path.resolve(defaultDir)) { + return { configDir: target, surfaces }; + } + + fs.mkdirSync(target, { recursive: true }); + const ledger = readProfileLedger(target, defaultDir); + + for (const name of CODEX_SHARED_DIRS) { + surfaces[`${name}/`] = linkSharedDir( + path.join(defaultDir, name), + path.join(target, name), + ); + } + for (const name of CODEX_SHARED_FILES) { + surfaces[name] = syncSharedFile({ + sourcePath: path.join(defaultDir, name), + targetPath: path.join(target, name), + surface: name, + ledger, + }); + } + + writeProfileLedger(target, ledger); + ensureCodexManagedHooksAt(target); + + const report = { configDir: target, surfaces }; + logReport("Codex", report); + return report; +} diff --git a/packages/host-service/src/app.ts b/packages/host-service/src/app.ts index d26c98c88ad..dc5c84fbb13 100644 --- a/packages/host-service/src/app.ts +++ b/packages/host-service/src/app.ts @@ -30,6 +30,7 @@ import { TerminalAgentStore, } from "./terminal-agents"; import { appRouter } from "./trpc/router"; +import { provisionSelectedAccounts } from "./trpc/router/usage/account-provisioning"; import { execGh as defaultExecGh, type ExecGh, @@ -239,6 +240,12 @@ export function createApp(options: CreateAppOptions): CreateAppResult { }).catch((err) => { console.warn("[host-service] archived-workspace reconcile failed:", err); }); + // Re-share the default account's Claude/Codex config into the selected + // provider profiles. Last: it touches no host state the sweeps above + // repair, and a slow filesystem must not delay them. + await provisionSelectedAccounts(db).catch((err) => { + console.warn("[host-service] account provisioning failed:", err); + }); })(); const wsAuth: MiddlewareHandler = async (c, next) => { diff --git a/packages/host-service/src/trpc/router/usage/account-provisioning.ts b/packages/host-service/src/trpc/router/usage/account-provisioning.ts new file mode 100644 index 00000000000..0a5c4779ac8 --- /dev/null +++ b/packages/host-service/src/trpc/router/usage/account-provisioning.ts @@ -0,0 +1,58 @@ +/** + * Keeps the selected provider accounts provisioned — see + * packages/agent-setup/src/provider-profiles.ts for what that means. Split + * from default-account.ts so the terminal's env-resolution path (loaded by + * node --test) doesn't pull the whole agent-setup surface in. + */ + +import { existsSync } from "node:fs"; +import { + provisionClaudeProfile, + provisionCodexProfile, +} from "@superset/agent-setup"; +import type { HostDb } from "../../../db/index.ts"; +import { getDefaultAccountSelections } from "./default-account.ts"; +import { shareClaudeProfileState } from "./profile-share.ts"; + +/** + * Everything a Claude account needs on this host: the shared session state + * (one `--resume` history across accounts — profile-share.ts) plus the + * config surfaces agent-setup owns (skills, plugins, settings, MCP servers). + */ +export async function provisionClaudeAccount(configDir: string): Promise { + shareClaudeProfileState(configDir); + await provisionClaudeProfile(configDir); +} + +/** + * Re-shares the default account's config into whichever profiles are + * currently selected. Runs at host boot so a profile keeps up with skills, + * plugins, and settings added since it was selected, and so one provisioned + * by an older build — or by a switch that failed halfway — is repaired + * before the next agent launches on it. + */ +export async function provisionSelectedAccounts(db: HostDb): Promise { + const { claudeConfigDir, codexHome } = getDefaultAccountSelections(db); + const targets: Array Promise]> = []; + // A pointer at a vanished dir is skipped, not recreated: agent launches + // already fall back to the system-default login in that case. + if (claudeConfigDir && existsSync(claudeConfigDir)) { + targets.push([ + claudeConfigDir, + () => provisionClaudeAccount(claudeConfigDir), + ]); + } + if (codexHome && existsSync(codexHome)) { + targets.push([codexHome, () => provisionCodexProfile(codexHome)]); + } + for (const [dir, provision] of targets) { + try { + await provision(); + } catch (error) { + console.warn( + `[host-service] provisioning the selected account ${dir} failed (continuing):`, + error, + ); + } + } +} diff --git a/packages/host-service/src/trpc/router/usage/profile-remove.test.ts b/packages/host-service/src/trpc/router/usage/profile-remove.test.ts index 8cea1f54535..c18dd048d19 100644 --- a/packages/host-service/src/trpc/router/usage/profile-remove.test.ts +++ b/packages/host-service/src/trpc/router/usage/profile-remove.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from "bun:test"; -import { homedir } from "node:os"; +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { assertRemovableProfileDir, @@ -58,3 +66,29 @@ describe("remove functions refuse protected dirs before touching the disk", () = ); }); }); + +// A provisioned profile links its capability dirs at the default account's +// (packages/agent-setup/src/provider-profiles.ts). Removing the profile must +// unlink those, never delete through them. +describe("removeClaudeProfile with shared-config links", () => { + it("leaves the default account's linked dirs intact", async () => { + const root = mkdtempSync(join(tmpdir(), "superset-profile-remove-")); + try { + const sharedSkills = join(root, "default-skills"); + mkdirSync(join(sharedSkills, "redesign"), { recursive: true }); + writeFileSync(join(sharedSkills, "redesign", "SKILL.md"), "# redesign"); + + // Under the home dir so the removal guards accept it. + const profile = join(homedir(), `.claude-remove-test-${process.pid}`); + mkdirSync(profile, { recursive: true }); + symlinkSync(sharedSkills, join(profile, "skills"), "dir"); + + await removeClaudeProfile(profile); + + expect(existsSync(profile)).toBe(false); + expect(existsSync(join(sharedSkills, "redesign", "SKILL.md"))).toBe(true); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/host-service/src/trpc/router/usage/profile-seed.test.ts b/packages/host-service/src/trpc/router/usage/profile-seed.test.ts deleted file mode 100644 index ddf242648ef..00000000000 --- a/packages/host-service/src/trpc/router/usage/profile-seed.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { seedClaudeProfileOnboarding } from "./profile-seed"; - -function makeProfile(state: unknown): string { - const dir = mkdtempSync(join(tmpdir(), "claude-profile-seed-")); - if (state !== undefined) { - writeFileSync(join(dir, ".claude.json"), JSON.stringify(state)); - } - return dir; -} - -function readState(dir: string): Record { - return JSON.parse(readFileSync(join(dir, ".claude.json"), "utf-8")); -} - -describe("seedClaudeProfileOnboarding", () => { - it("marks onboarding complete and preserves the CLI's other state", () => { - const dir = makeProfile({ - oauthAccount: { emailAddress: "a@b.c" }, - numStartups: 3, - }); - seedClaudeProfileOnboarding(dir); - const state = readState(dir); - expect(state.hasCompletedOnboarding).toBe(true); - expect(state.oauthAccount).toEqual({ emailAddress: "a@b.c" }); - expect(state.numStartups).toBe(3); - }); - - it("never overwrites a theme the profile already chose", () => { - const dir = makeProfile({ theme: "light" }); - seedClaudeProfileOnboarding(dir); - expect(readState(dir).theme).toBe("light"); - }); - - it("does nothing when onboarding is already complete", () => { - const dir = makeProfile({ hasCompletedOnboarding: true, theme: "light" }); - const before = readFileSync(join(dir, ".claude.json"), "utf-8"); - seedClaudeProfileOnboarding(dir); - expect(readFileSync(join(dir, ".claude.json"), "utf-8")).toBe(before); - }); - - it("does not create a state file where none exists (no login yet)", () => { - const dir = makeProfile(undefined); - seedClaudeProfileOnboarding(dir); - expect(() => readState(dir)).toThrow(); - }); -}); diff --git a/packages/host-service/src/trpc/router/usage/profile-seed.ts b/packages/host-service/src/trpc/router/usage/profile-seed.ts deleted file mode 100644 index 7db17770583..00000000000 --- a/packages/host-service/src/trpc/router/usage/profile-seed.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Marks a secondary Claude profile's onboarding as complete so its first - * agent launch goes straight to the prompt instead of the first-boot wizard - * (where a stray Enter can start a login that silently rebinds the profile). - * - * This edits the CLI's state file (`.claude.json`), never a credential - * store, and only ever adds two preference keys — everything else is left - * exactly as the CLI wrote it. - */ - -import { readFileSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -export function seedClaudeProfileOnboarding(configDir: string): void { - const statePath = join(configDir, ".claude.json"); - let state: Record; - try { - state = JSON.parse(readFileSync(statePath, "utf-8")); - } catch { - // No parsable state file means no completed login yet — nothing to seed. - return; - } - if (state.hasCompletedOnboarding === true) return; - state.hasCompletedOnboarding = true; - if (state.theme === undefined) { - // Carry the default profile's theme so the first launch looks familiar. - try { - const defaultState = JSON.parse( - readFileSync(join(homedir(), ".claude.json"), "utf-8"), - ) as { theme?: unknown }; - if (typeof defaultState.theme === "string") { - state.theme = defaultState.theme; - } - } catch { - // Theme stays unset; the CLI falls back to its own default. - } - } - writeFileSync(statePath, JSON.stringify(state, null, 2)); -} diff --git a/packages/host-service/src/trpc/router/usage/profile-share.test.ts b/packages/host-service/src/trpc/router/usage/profile-share.test.ts index 49a102c3764..42dc0d94009 100644 --- a/packages/host-service/src/trpc/router/usage/profile-share.test.ts +++ b/packages/host-service/src/trpc/router/usage/profile-share.test.ts @@ -26,6 +26,14 @@ function isLinkTo(path: string, target: string): boolean { return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target; } +function lstatOrNull(path: string) { + try { + return lstatSync(path); + } catch { + return null; + } +} + describe("shareableProfileDir", () => { it("refuses the default homes and the main home itself", () => { const home = homedir(); @@ -49,20 +57,16 @@ describe("shareableProfileDir", () => { }); describe("shareClaudeProfileState", () => { - it("links a fresh profile's config and session entries into main", () => { + it("links a fresh profile's session entries into main", () => { const { profile, main } = makeDirs(); - writeFileSync(join(main, "settings.json"), "{}"); shareClaudeProfileState(profile, main); - for (const name of ["agents", "skills", "projects", "sessions"]) { + for (const name of ["projects", "sessions", "file-history", "todos"]) { expect(isLinkTo(join(profile, name), join(main, name))).toBe(true); expect(lstatSync(join(main, name)).isDirectory()).toBe(true); } expect( isLinkTo(join(profile, "history.jsonl"), join(main, "history.jsonl")), ).toBe(true); - expect( - isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), - ).toBe(true); }); it("merges existing session trees into main before linking", () => { @@ -139,55 +143,24 @@ describe("shareClaudeProfileState", () => { ); }); - it("backs up divergent settings beside the link", () => { + it("leaves config surfaces to agent-setup provisioning", () => { const { profile, main } = makeDirs(); - writeFileSync(join(main, "settings.json"), '{"main":true}'); - writeFileSync(join(profile, "settings.json"), '{"profile":true}'); - shareClaudeProfileState(profile, main); - expect( - isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), - ).toBe(true); - const backup = readdirSync(profile).find((name) => - name.startsWith("settings.json.pre-share-"), - ); - expect(backup).toBeDefined(); - expect(readFileSync(join(profile, backup as string), "utf-8")).toBe( - '{"profile":true}', - ); - }); - - it("promotes the profile's settings when main has none", () => { - const { profile, main } = makeDirs(); - writeFileSync(join(profile, "settings.json"), '{"profile":true}'); - shareClaudeProfileState(profile, main); - expect(readFileSync(join(main, "settings.json"), "utf-8")).toBe( - '{"profile":true}', - ); - expect( - isLinkTo(join(profile, "settings.json"), join(main, "settings.json")), - ).toBe(true); - }); - - it("respects config a profile deliberately diverged", () => { - const { profile, main } = makeDirs(); - mkdirSync(join(profile, "agents")); - writeFileSync(join(profile, "agents", "mine.md"), "custom"); - writeFileSync(join(profile, "CLAUDE.md"), "profile memory"); + writeFileSync(join(main, "settings.json"), "{}"); + writeFileSync(join(main, "CLAUDE.md"), "memory"); + mkdirSync(join(main, "agents")); shareClaudeProfileState(profile, main); - expect(lstatSync(join(profile, "agents")).isSymbolicLink()).toBe(false); - expect(lstatSync(join(profile, "CLAUDE.md")).isSymbolicLink()).toBe(false); - expect(readFileSync(join(profile, "CLAUDE.md"), "utf-8")).toBe( - "profile memory", - ); + for (const name of ["settings.json", "CLAUDE.md", "agents", "skills"]) { + expect(lstatOrNull(join(profile, name))).toBeNull(); + } }); it("leaves existing symlinks alone, wherever they point", () => { const { profile, main } = makeDirs(); const elsewhere = join(profile, "elsewhere"); mkdirSync(elsewhere); - symlinkSync(elsewhere, join(profile, "skills")); + symlinkSync(elsewhere, join(profile, "projects")); shareClaudeProfileState(profile, main); - expect(readlinkSync(join(profile, "skills"))).toBe(elsewhere); + expect(readlinkSync(join(profile, "projects"))).toBe(elsewhere); }); it("never touches identity or runtime entries", () => { diff --git a/packages/host-service/src/trpc/router/usage/profile-share.ts b/packages/host-service/src/trpc/router/usage/profile-share.ts index 4eae08585ac..b5e7ff5792f 100644 --- a/packages/host-service/src/trpc/router/usage/profile-share.ts +++ b/packages/host-service/src/trpc/router/usage/profile-share.ts @@ -1,20 +1,23 @@ /** - * Shares account-agnostic Claude state between a secondary profile dir and - * the default `~/.claude` home via symlinks, so every login sees one - * conversation history, one prompt history, and one config set — switching - * accounts stops meaning losing `--resume`, skills, and settings. + * Shares session state between a secondary Claude profile dir and the + * default `~/.claude` home via symlinks, so every login sees one + * conversation history — switching accounts stops meaning losing + * `--resume` and the prompt history. * - * Identity stays per-profile by design: `.claude.json` (oauthAccount, - * per-project trust, MCP auth) and the credential stores are never touched. - * Runtime dirs (daemon/, cache/, telemetry/, backups/) stay local too — the - * daemon's locks and sockets must not be shared between CLIs. + * Only surfaces a symlink can survive are shared here: directories, and + * `history.jsonl`, which the CLI appends to in place. Config files like + * settings.json and `.claude.json` are written with write-tmp-then-rename — + * a rename would replace the symlink with a real file and silently fork the + * config — so those (plus skills, plugins, MCP servers) belong to + * agent-setup's ledger-based profile provisioning, not to this module. + * Identity (`.claude.json`, credential stores) and runtime dirs (daemon/, + * cache/, telemetry/, backups/) always stay per-profile. * * Existing real state is merged, not clobbered. Session trees are renamed * aside, the symlink lands immediately, and files then move into `~/.claude` * one by one: renames preserve inodes, so a live session's open transcripts * stay valid and its paths resolve through the new link. The prompt history - * is appended; a divergent settings.json is backed up beside the link. - * Anything that cannot merge safely is left where it is. + * is appended. Anything that cannot merge safely is left where it is. */ import { @@ -27,17 +30,10 @@ import { rmdirSync, symlinkSync, unlinkSync, - writeFileSync, } from "node:fs"; import { homedir, platform } from "node:os"; import { join, resolve } from "node:path"; -/** Config the user authors once and expects everywhere. Linked only when - * the profile has nothing of its own (absent or empty) — a profile that - * deliberately diverged keeps its version. */ -const CONFIG_DIRS = ["agents", "commands", "skills", "hooks", "plugins"]; -const CONFIG_FILES = ["CLAUDE.md"]; - /** Session-scoped state keyed by session UUID or cwd slug: safe to merge * file-by-file, since two profiles' sessions collide no more than two * concurrent sessions in one dir already do. */ @@ -110,28 +106,6 @@ function moveTreeInto(srcDir: string, dstDir: string): void { } } -function linkConfigDir(profile: string, main: string, name: string): void { - const src = join(profile, name); - const info = lstatOrNull(src); - if (info?.isSymbolicLink()) return; - if (info && !info.isDirectory()) return; - if (info && readdirSync(src).length > 0) return; - mkdirSync(join(main, name), { recursive: true }); - if (info) rmdirSync(src); - symlinkSync(join(main, name), src); -} - -function linkConfigFile(profile: string, main: string, name: string): void { - const src = join(profile, name); - const target = join(main, name); - const info = lstatOrNull(src); - if (info?.isSymbolicLink()) return; - if (info && (!info.isFile() || info.size > 0)) return; - if (!lstatOrNull(target)) writeFileSync(target, "", { flag: "wx" }); - if (info) unlinkSync(src); - symlinkSync(target, src); -} - function mergeAndLinkSessionDir( profile: string, main: string, @@ -194,24 +168,6 @@ function mergeAndLinkHistory(profile: string, main: string): void { unlinkSync(pending); } -function linkSettings(profile: string, main: string): void { - const src = join(profile, "settings.json"); - const dst = join(main, "settings.json"); - const info = lstatOrNull(src); - if (info?.isSymbolicLink()) return; - if (info && !info.isFile()) return; - const dstInfo = lstatOrNull(dst); - if (dstInfo && !dstInfo.isFile()) return; - if (!info && !dstInfo) return; - if (!dstInfo) { - // Main has no settings yet — the profile's become the shared ones. - renameSync(src, dst); - } else if (info) { - renameSync(src, `${src}.pre-share-${Date.now()}`); - } - symlinkSync(dst, src); -} - /** * Best-effort per entry: one unmergeable path must not stop the rest, and a * partially shared profile is strictly better than an unshared one. @@ -226,13 +182,10 @@ export function shareClaudeProfileState( if (!profile) return; const main = resolve(mainHome); const steps: Array<() => void> = [ - ...CONFIG_DIRS.map((name) => () => linkConfigDir(profile, main, name)), - ...CONFIG_FILES.map((name) => () => linkConfigFile(profile, main, name)), ...SESSION_DIRS.map( (name) => () => mergeAndLinkSessionDir(profile, main, name), ), () => mergeAndLinkHistory(profile, main), - () => linkSettings(profile, main), ]; for (const step of steps) { try { diff --git a/packages/host-service/src/trpc/router/usage/usage.ts b/packages/host-service/src/trpc/router/usage/usage.ts index b13fa3a7701..dd3e95ea2b3 100644 --- a/packages/host-service/src/trpc/router/usage/usage.ts +++ b/packages/host-service/src/trpc/router/usage/usage.ts @@ -1,16 +1,14 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { basename, join } from "node:path"; -import { - ensureClaudeManagedHooksAt, - ensureCodexManagedHooksAt, -} from "@superset/agent-setup"; +import { provisionCodexProfile } from "@superset/agent-setup"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { projects, workspaces } from "../../../db/schema"; import { usageHistoryTask } from "../../../workers/tasks/usage"; import { protectedProcedure, queryProcedure, router } from "../../index"; import { offLoop } from "../../off-loop"; +import { provisionClaudeAccount } from "./account-provisioning"; import { fetchClaudeAccounts, readDefaultLoginEmail } from "./claude"; import { fetchCodexAccounts } from "./codex"; import { @@ -18,8 +16,6 @@ import { setDefaultAccountSelection, } from "./default-account"; import { removeClaudeProfile, removeCodexHome } from "./profile-remove"; -import { seedClaudeProfileOnboarding } from "./profile-seed"; -import { shareClaudeProfileState } from "./profile-share"; import { discoverClaudeProfiles, discoverCodexHomes } from "./profiles"; import type { UsageAccount } from "./types"; @@ -147,25 +143,22 @@ export const usageRouter = router({ } } setDefaultAccountSelection(ctx.db, input.provider, input.selection); - // Secondary profiles read hooks from their own dir, so agents launched - // there would otherwise lose lifecycle/status reporting; fresh Claude - // profiles also need onboarding marked done or the first launch runs - // the first-boot wizard. Claude profiles first get their shared state - // linked into ~/.claude (one history/settings across accounts — - // profile-share.ts), so the hook merge lands in the shared settings - // file. Best-effort: a failed merge must not undo the switch. + // A profile dir is a whole config root, not just a login: without + // provisioning, agents launched there lose the user's skills, + // plugins, MCP servers and settings along with Superset's lifecycle + // hooks — and, for Claude, the shared session history. Best-effort — + // a failed share must not undo the switch, and provisioning retries + // on the next switch and at host boot. if (input.selection !== null) { try { - if (input.provider === "claude") { - shareClaudeProfileState(input.selection); - ensureClaudeManagedHooksAt(input.selection); - seedClaudeProfileOnboarding(input.selection); - } else { - ensureCodexManagedHooksAt(input.selection); - } - } catch { - // Agents still run without hooks; provisioning retries on the - // next switch. + await (input.provider === "claude" + ? provisionClaudeAccount(input.selection) + : provisionCodexProfile(input.selection)); + } catch (error) { + console.warn( + `[host-service] provisioning ${input.provider} account ${input.selection} failed (continuing):`, + error, + ); } } return { success: true as const }; @@ -218,23 +211,33 @@ export const usageRouter = router({ }), /** - * One-time preparation for a freshly added Claude profile: link its shared - * state into ~/.claude (profile-share.ts) and mark onboarding complete so - * the first agent launch doesn't open the first-boot wizard. Only accepts - * discovered profile dirs. + * Preparation for a freshly added profile: share the default account's + * config into it (and, for Claude, mark onboarding complete), so its first + * agent launch opens the prompt with the user's usual setup instead of the + * first-boot wizard on an empty install. Only accepts discovered profile + * dirs. */ - prepareClaudeProfile: protectedProcedure - .input(z.object({ configDir: z.string() })) + prepareAccount: protectedProcedure + .input( + z.object({ + provider: z.enum(["claude", "codex"]), + selection: z.string(), + }), + ) .mutation(async ({ input }) => { - const profiles = await discoverClaudeProfiles(); - if (!profiles.some((profile) => profile.configDir === input.configDir)) { + const discovered = + input.provider === "claude" + ? (await discoverClaudeProfiles()).map((profile) => profile.configDir) + : (await discoverCodexHomes()).map((home) => home.home); + if (!discovered.includes(input.selection)) { throw new TRPCError({ code: "BAD_REQUEST", - message: `No Claude profile found at ${input.configDir}.`, + message: `No ${input.provider} profile found at ${input.selection}.`, }); } - shareClaudeProfileState(input.configDir); - seedClaudeProfileOnboarding(input.configDir); + await (input.provider === "claude" + ? provisionClaudeAccount(input.selection) + : provisionCodexProfile(input.selection)); return { success: true as const }; }), From 637f6e76be9f40f4839e1cf7455742062e644922 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 18 Aug 2026 22:02:01 -0700 Subject: [PATCH 3/7] refactor(host-service,agent-setup): pre-merge cleanup for account provisioning - Rename the host-service session module profile-share.ts -> session-share.ts (shareClaudeSessionState): its old name was one character away from agent-setup's profile-sharing.ts, which owns a different mechanism. - Guard provisionClaudeProfile/provisionCodexProfile against default homes beyond the share source (~, ~/.config, ~/.config/claude): discovery excludes them, but a CLAUDE_CONFIG_DIR env entry can name one directly, and provisioning one would link a default home into itself. - Fix docs/agent-tooling.md: it still said transcripts and session state are never shared, contradicting the session-sharing half of this PR. --- docs/agent-tooling.md | 10 ++++++-- .../agent-setup/src/provider-profiles.test.ts | 13 +++++++++++ packages/agent-setup/src/provider-profiles.ts | 23 +++++++++++++++++-- .../trpc/router/usage/account-provisioning.ts | 6 ++--- .../trpc/router/usage/history/aggregate.ts | 2 +- ...le-share.test.ts => session-share.test.ts} | 22 +++++++++--------- .../{profile-share.ts => session-share.ts} | 2 +- 7 files changed, 58 insertions(+), 20 deletions(-) rename packages/host-service/src/trpc/router/usage/{profile-share.test.ts => session-share.test.ts} (91%) rename packages/host-service/src/trpc/router/usage/{profile-share.ts => session-share.ts} (99%) diff --git a/docs/agent-tooling.md b/docs/agent-tooling.md index 2106b770ab1..3bb2e533478 100644 --- a/docs/agent-tooling.md +++ b/docs/agent-tooling.md @@ -54,8 +54,14 @@ non-default profile from the default account (`~/.claude`, `~/.codex`): file symlink. - Copies and merged keys are recorded in `/.superset-profile.json`, so anything the user changes inside a profile is never overwritten by a later provision. -- Per-account and never shared: credentials, `oauthAccount`/`userID`, transcripts and session - state, and Superset's own lifecycle hooks (written per profile). +- Claude session state (`projects/`, `sessions/`, `history.jsonl`, …) is shared too — by symlink, + which is safe there because transcripts are append-only — so every account sees one + conversation history and `--resume` list + (`packages/host-service/src/trpc/router/usage/session-share.ts`; existing trees are merged in, + live sessions included). +- Per-account and never shared: credentials, `oauthAccount`/`userID` identity and per-project + state in `.claude.json`, auth-related settings keys, and Superset's own lifecycle hooks + (written per profile). Provisioning is idempotent and runs when an account is added, when one is selected, and at host boot for the selected accounts (`usage/account-provisioning.ts`). diff --git a/packages/agent-setup/src/provider-profiles.test.ts b/packages/agent-setup/src/provider-profiles.test.ts index c5137b8d8f4..f5540202ad0 100644 --- a/packages/agent-setup/src/provider-profiles.test.ts +++ b/packages/agent-setup/src/provider-profiles.test.ts @@ -223,3 +223,16 @@ describe("provisionCodexProfile", () => { ).toContain("notify.sh"); }); }); + +describe("default homes are never provisioning targets", () => { + it("refuses ~/.config/claude and the home dir itself", async () => { + const configClaude = path.join(HOME, ".config", "claude"); + mkdirSync(configClaude, { recursive: true }); + for (const target of [configClaude, HOME, path.join(HOME, ".config")]) { + const report = await provisionClaudeProfile(target, { homeDir: HOME }); + expect(report.surfaces).toEqual({}); + } + expect(existsSync(path.join(configClaude, "skills"))).toBe(false); + expect(existsSync(path.join(HOME, ".superset-profile.json"))).toBe(false); + }); +}); diff --git a/packages/agent-setup/src/provider-profiles.ts b/packages/agent-setup/src/provider-profiles.ts index e31a2f7a8f1..933e964d366 100644 --- a/packages/agent-setup/src/provider-profiles.ts +++ b/packages/agent-setup/src/provider-profiles.ts @@ -46,6 +46,25 @@ function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +/** + * True for the share's source and the other default homes — provisioning a + * default home would link it into itself (discovery excludes them, but a + * CLAUDE_CONFIG_DIR env entry can name one directly). + */ +function isProtectedTarget( + target: string, + homeDir: string, + defaultDir: string, +): boolean { + const protectedDirs = [ + homeDir, + defaultDir, + path.join(homeDir, ".config"), + path.join(homeDir, ".config", "claude"), + ]; + return protectedDirs.some((dir) => target === path.resolve(dir)); +} + function logReport(provider: string, report: ProfileProvisionReport): void { const summary = Object.entries(report.surfaces) .map(([surface, outcome]) => `${surface}=${outcome}`) @@ -149,7 +168,7 @@ export async function provisionClaudeProfile( const defaultDir = path.join(homeDir, ".claude"); const target = path.resolve(configDir); const surfaces: Record = {}; - if (target === path.resolve(defaultDir)) { + if (isProtectedTarget(target, homeDir, defaultDir)) { return { configDir: target, surfaces }; } @@ -241,7 +260,7 @@ export async function provisionCodexProfile( const defaultDir = defaultCodexHome(homeDir); const target = path.resolve(home); const surfaces: Record = {}; - if (target === path.resolve(defaultDir)) { + if (isProtectedTarget(target, homeDir, defaultDir)) { return { configDir: target, surfaces }; } diff --git a/packages/host-service/src/trpc/router/usage/account-provisioning.ts b/packages/host-service/src/trpc/router/usage/account-provisioning.ts index 0a5c4779ac8..ca8b2a13225 100644 --- a/packages/host-service/src/trpc/router/usage/account-provisioning.ts +++ b/packages/host-service/src/trpc/router/usage/account-provisioning.ts @@ -12,15 +12,15 @@ import { } from "@superset/agent-setup"; import type { HostDb } from "../../../db/index.ts"; import { getDefaultAccountSelections } from "./default-account.ts"; -import { shareClaudeProfileState } from "./profile-share.ts"; +import { shareClaudeSessionState } from "./session-share.ts"; /** * Everything a Claude account needs on this host: the shared session state - * (one `--resume` history across accounts — profile-share.ts) plus the + * (one `--resume` history across accounts — session-share.ts) plus the * config surfaces agent-setup owns (skills, plugins, settings, MCP servers). */ export async function provisionClaudeAccount(configDir: string): Promise { - shareClaudeProfileState(configDir); + shareClaudeSessionState(configDir); await provisionClaudeProfile(configDir); } diff --git a/packages/host-service/src/trpc/router/usage/history/aggregate.ts b/packages/host-service/src/trpc/router/usage/history/aggregate.ts index 5a8dcfc4060..09c3e57400e 100644 --- a/packages/host-service/src/trpc/router/usage/history/aggregate.ts +++ b/packages/host-service/src/trpc/router/usage/history/aggregate.ts @@ -190,7 +190,7 @@ export async function computeUsageHistory( for (const profile of claudeProfiles) claudeHomes.add(profile.configDir); // Shared-history profiles symlink their projects/ into ~/.claude (see - // profile-share.ts), so two homes can name the same tree under different + // session-share.ts), so two homes can name the same tree under different // paths — resolve every scan root and dedupe by real path, or the tree // gets walked and parsed once per profile. const resolveRoots = async (roots: string[]): Promise => { diff --git a/packages/host-service/src/trpc/router/usage/profile-share.test.ts b/packages/host-service/src/trpc/router/usage/session-share.test.ts similarity index 91% rename from packages/host-service/src/trpc/router/usage/profile-share.test.ts rename to packages/host-service/src/trpc/router/usage/session-share.test.ts index 42dc0d94009..388078e85d1 100644 --- a/packages/host-service/src/trpc/router/usage/profile-share.test.ts +++ b/packages/host-service/src/trpc/router/usage/session-share.test.ts @@ -11,10 +11,10 @@ import { } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; -import { shareableProfileDir, shareClaudeProfileState } from "./profile-share"; +import { shareableProfileDir, shareClaudeSessionState } from "./session-share"; function makeDirs(): { profile: string; main: string } { - const root = mkdtempSync(join(tmpdir(), "claude-profile-share-")); + const root = mkdtempSync(join(tmpdir(), "claude-session-share-")); const profile = join(root, "profile"); const main = join(root, "main"); mkdirSync(profile); @@ -56,10 +56,10 @@ describe("shareableProfileDir", () => { }); }); -describe("shareClaudeProfileState", () => { +describe("shareClaudeSessionState", () => { it("links a fresh profile's session entries into main", () => { const { profile, main } = makeDirs(); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); for (const name of ["projects", "sessions", "file-history", "todos"]) { expect(isLinkTo(join(profile, name), join(main, name))).toBe(true); expect(lstatSync(join(main, name)).isDirectory()).toBe(true); @@ -75,7 +75,7 @@ describe("shareClaudeProfileState", () => { writeFileSync(join(profile, "projects", "-repo-a", "s1.jsonl"), "a"); mkdirSync(join(main, "projects", "-repo-b"), { recursive: true }); writeFileSync(join(main, "projects", "-repo-b", "s2.jsonl"), "b"); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect(isLinkTo(join(profile, "projects"), join(main, "projects"))).toBe( true, ); @@ -97,7 +97,7 @@ describe("shareClaudeProfileState", () => { writeFileSync(join(profile, "projects", "-repo", "s.jsonl"), "profile"); mkdirSync(join(main, "projects", "-repo"), { recursive: true }); writeFileSync(join(main, "projects", "-repo", "s.jsonl"), "main"); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect(isLinkTo(join(profile, "projects"), join(main, "projects"))).toBe( true, ); @@ -123,7 +123,7 @@ describe("shareClaudeProfileState", () => { ); symlinkSync(join(main, "projects"), join(profile, "projects")); mkdirSync(join(main, "projects")); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect( readFileSync(join(main, "projects", "-repo", "s.jsonl"), "utf-8"), ).toBe("leftover"); @@ -134,7 +134,7 @@ describe("shareClaudeProfileState", () => { const { profile, main } = makeDirs(); writeFileSync(join(main, "history.jsonl"), '{"m":1}\n'); writeFileSync(join(profile, "history.jsonl"), '{"p":1}\n'); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect( isLinkTo(join(profile, "history.jsonl"), join(main, "history.jsonl")), ).toBe(true); @@ -148,7 +148,7 @@ describe("shareClaudeProfileState", () => { writeFileSync(join(main, "settings.json"), "{}"); writeFileSync(join(main, "CLAUDE.md"), "memory"); mkdirSync(join(main, "agents")); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); for (const name of ["settings.json", "CLAUDE.md", "agents", "skills"]) { expect(lstatOrNull(join(profile, name))).toBeNull(); } @@ -159,7 +159,7 @@ describe("shareClaudeProfileState", () => { const elsewhere = join(profile, "elsewhere"); mkdirSync(elsewhere); symlinkSync(elsewhere, join(profile, "projects")); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect(readlinkSync(join(profile, "projects"))).toBe(elsewhere); }); @@ -168,7 +168,7 @@ describe("shareClaudeProfileState", () => { writeFileSync(join(profile, ".claude.json"), '{"oauthAccount":{}}'); writeFileSync(join(profile, ".credentials.json"), "secret"); mkdirSync(join(profile, "daemon")); - shareClaudeProfileState(profile, main); + shareClaudeSessionState(profile, main); expect(lstatSync(join(profile, ".claude.json")).isSymbolicLink()).toBe( false, ); diff --git a/packages/host-service/src/trpc/router/usage/profile-share.ts b/packages/host-service/src/trpc/router/usage/session-share.ts similarity index 99% rename from packages/host-service/src/trpc/router/usage/profile-share.ts rename to packages/host-service/src/trpc/router/usage/session-share.ts index b5e7ff5792f..9593451cfb2 100644 --- a/packages/host-service/src/trpc/router/usage/profile-share.ts +++ b/packages/host-service/src/trpc/router/usage/session-share.ts @@ -172,7 +172,7 @@ function mergeAndLinkHistory(profile: string, main: string): void { * Best-effort per entry: one unmergeable path must not stop the rest, and a * partially shared profile is strictly better than an unshared one. */ -export function shareClaudeProfileState( +export function shareClaudeSessionState( configDir: string, mainHome: string = join(homedir(), ".claude"), ): void { From 6d282707b58d4119868b471a021a4a7c31a4568b Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 18 Aug 2026 22:15:12 -0700 Subject: [PATCH 4/7] docs: add Usage & Accounts page for quota, token spend, and multi-account --- apps/docs/content/docs/meta.json | 1 + apps/docs/content/docs/usage.mdx | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 apps/docs/content/docs/usage.mdx diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index bc49f6a0d46..8426b6f3a9d 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -24,6 +24,7 @@ "browser", "agent-integration", "agent-status", + "usage", "tasks", "remote-workspaces", "automations", diff --git a/apps/docs/content/docs/usage.mdx b/apps/docs/content/docs/usage.mdx new file mode 100644 index 00000000000..5551456ac42 --- /dev/null +++ b/apps/docs/content/docs/usage.mdx @@ -0,0 +1,32 @@ +--- +title: Usage & Accounts +description: Track quota and token spend across providers, and run multiple Claude Code or Codex accounts +--- + +The **Usage** page shows what your agents are consuming — official subscription quota, token spend, machine resources — and manages the provider accounts they run on. + +## Quota + +Each Claude Code and Codex login on your machine gets a card with its official limits: the 5-hour session window and weekly windows, refreshed every 5 minutes. + +- Superset reads the tokens already on your machine, read-only. It never refreshes or rewrites credentials — the provider CLIs own every login end to end. +- Org-managed and education plans don't expose limits; those cards show **Unavailable**. +- **Sign-in expired** on a card means the login needs `/login` again — use **Switch sign-in** on the card's menu. + +## Token usage + +Spend is estimated from the session logs already on disk, priced at full API rates — on a subscription your real cost is $0; the number shows what the work would have cost. + +- Breakdowns by model, workspace/project, and day, with drilldowns per workspace and per model. +- **Machine resources** (toggle at the top) shows what your workspaces are doing to the machine itself. + +## Multiple accounts + +Run several subscriptions side by side — a second personal Max account, a work account — and pick which one new agents use. + +- **Add account** signs a second subscription into its own profile. Your current login is untouched. +- **Make default** points new terminals and agents at that account. Running sessions keep their current account until restarted. +- Your setup comes along: skills, plugins, MCP servers, and settings are shared across accounts, and there's one conversation history — `--resume` shows the same sessions whichever account is active. +- Only the login itself stays per account. Removing an account deletes its profile and credentials, never your shared setup or history. + +Under the hood, a profile is just a config dir the CLI is pointed at (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`), injected into new terminals and agent launches — so switching works for agents you launch by hand in a Superset terminal too. From 8e1f489d27a6d3c2b70187c882ee8b0ab9e5d343 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 18 Aug 2026 22:20:31 -0700 Subject: [PATCH 5/7] docs: screenshot of the multi-account cards on the Usage page --- apps/docs/content/docs/usage.mdx | 2 ++ apps/docs/public/images/usage-accounts.png | Bin 0 -> 45545 bytes 2 files changed, 2 insertions(+) create mode 100644 apps/docs/public/images/usage-accounts.png diff --git a/apps/docs/content/docs/usage.mdx b/apps/docs/content/docs/usage.mdx index 5551456ac42..33399b69dbd 100644 --- a/apps/docs/content/docs/usage.mdx +++ b/apps/docs/content/docs/usage.mdx @@ -24,6 +24,8 @@ Spend is estimated from the session logs already on disk, priced at full API rat Run several subscriptions side by side — a second personal Max account, a work account — and pick which one new agents use. +![Two Claude Code accounts with quota meters, one marked as the default](/images/usage-accounts.png) + - **Add account** signs a second subscription into its own profile. Your current login is untouched. - **Make default** points new terminals and agents at that account. Running sessions keep their current account until restarted. - Your setup comes along: skills, plugins, MCP servers, and settings are shared across accounts, and there's one conversation history — `--resume` shows the same sessions whichever account is active. diff --git a/apps/docs/public/images/usage-accounts.png b/apps/docs/public/images/usage-accounts.png new file mode 100644 index 0000000000000000000000000000000000000000..b9d9006f844189d41f33ead472f0145c6b36d375 GIT binary patch literal 45545 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 literal 0 HcmV?d00001 From 8c1561580b06bc2fc2c098c2fa61d0d08129b4f7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 19 Aug 2026 17:14:03 -0700 Subject: [PATCH 6/7] feat(desktop): radio-style default selection on usage account cards When a provider has several accounts the cards now read as a radio group: the default gets a leading check, an accent border, and the others a selectable circle. The Make default action moves from a hover-revealed header button to an always-visible footer row, which also gives the credits/extra-usage line a proper home instead of cramming it next to the source label. The overflow menu shows without hover, and the switch toast is shorter. A lone card keeps a plain footer label instead of selection chrome. --- .../usage/components/UsageView/UsageView.tsx | 99 +++++++++++++----- apps/docs/public/images/usage-accounts.png | Bin 45545 -> 84412 bytes 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx index cabb58a63f5..abf04e44306 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx @@ -10,6 +10,8 @@ import { cn } from "@superset/ui/utils"; import { useState } from "react"; import { LuCheck, + LuCircle, + LuCircleCheck, LuCopy, LuEllipsis, LuPlus, @@ -86,12 +88,16 @@ function creditsLine(account: UsageAccount): string | null { return null; } +const DEFAULT_TITLE = + "New terminals and agents use this account. Existing ones keep theirs."; + function AccountCard({ account, onMakeDefault, onSwitchSignIn, onRemove, isSwitching, + selectable, }: { account: UsageAccount; onMakeDefault: () => void; @@ -99,14 +105,41 @@ function AccountCard({ /** Null on the system-default card — the main login is never removable. */ onRemove: (() => void) | null; isSwitching: boolean; + /** True when the provider has several accounts, so the cards read as a + * radio group: the default gets a check + accent border, the rest get a + * selectable circle. */ + selectable: boolean; }) { const credits = creditsLine(account); const { copyToClipboard, copied } = useCopyToClipboard(); const expiredCommand = account.status === "token_expired" ? switchSignInCommand(account) : null; return ( -
+
+ {selectable && + (account.isDefault ? ( + + + + ) : ( + + ))} {account.email ?? PROVIDER_LABELS[account.provider]} @@ -124,38 +157,17 @@ function AccountCard({ : "Unavailable"} )} - {account.isDefault ? ( - - Default - - ) : ( - - )} {/* Source label always shows — it is the only thing that tells two profiles of the same account apart. */} - {credits - ? `${credits} · ${account.sourceLabel}` - : account.sourceLabel} + {account.sourceLabel} @@ -205,6 +217,39 @@ function AccountCard({ {account.statusDetail ?? "Usage unavailable."}
)} + {/* The radio + accent border already mark the default when the cards + read as a group; the footer label only carries it for a lone card. */} + {(!account.isDefault || !selectable || credits) && ( +
+ {account.isDefault ? ( + !selectable && ( + + + Default for new agents + + ) + ) : ( + + )} + {credits && ( + + {credits} + + )} +
+ )}
); } @@ -231,10 +276,9 @@ export function UsageView({ hostUrl }: { hostUrl: string | null }) { { onSuccess: () => { toast.success( - `New ${PROVIDER_LABELS[account.provider]} terminals and agents will use ${account.email ?? account.sourceLabel}.`, + `New ${PROVIDER_LABELS[account.provider]} agents will use ${account.email ?? account.sourceLabel}.`, { - description: - "Your skills, plugins, MCP servers, and settings come along. Running sessions keep their current account — restart them to switch.", + description: "Restart your sessions to use it.", }, ); }, @@ -333,6 +377,7 @@ export function UsageView({ hostUrl }: { hostUrl: string | null }) { : () => setRemoveTarget(account) } isSwitching={setDefault.isPending} + selectable={providerAccounts.length > 1} /> ))}
diff --git a/apps/docs/public/images/usage-accounts.png b/apps/docs/public/images/usage-accounts.png index b9d9006f844189d41f33ead472f0145c6b36d375..c6343cf5fd25e34ff32faf02dfd4cba59b6055dc 100644 GIT binary patch literal 84412 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 From e5562e1eb8f38a2ef52a5da9b864f0d976614d28 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 19 Aug 2026 17:34:10 -0700 Subject: [PATCH 7/7] feat(agent-setup,host-service): account switches reach existing terminals at agent relaunch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default account was baked into the PTY env at terminal spawn, so a switch in the Usage tab only reached brand-new terminals — exiting and relaunching claude in an open terminal kept the old account. The host now publishes the selection to $SUPERSET_HOME_DIR/state/default-{claude-config-dir,codex-home} (on switch and healed at boot), and the claude/codex wrappers re-resolve it on every launch: inside Superset terminals only, never over a value the user exported by hand (the spawn-time injection carries a SUPERSET_DEFAULT_* twin so the wrapper can tell its own value apart), falling back to the system default when the pointer is empty or the profile dir vanished. Verified end to end in one dev-app terminal: claude auth status flips accounts across a Usage-tab switch with no terminal restart. Also addresses review findings: default-home guards now compare real paths so a symlink alias of ~/.claude cannot be provisioned into itself; provisionCodexProfile's homeDir override wins over an ambient CODEX_HOME; history.jsonl merges insert a separator when the target lacks a trailing newline; the removal test uses a unique temp profile; and the docs no longer claim subscription work costs $0 or that Codex history is shared. --- .../usage/components/UsageView/UsageView.tsx | 4 +- apps/docs/content/docs/usage.mdx | 10 +- docs/agent-tooling.md | 12 +- .../agent-wrappers-claude-codex-opencode.ts | 24 ++-- .../agent-setup/src/agent-wrappers-common.ts | 28 +++++ .../agent-setup/src/agent-wrappers.test.ts | 6 +- .../src/default-account-resolver.test.ts | 109 ++++++++++++++++++ packages/agent-setup/src/provider-profiles.ts | 22 +++- .../host-service/src/terminal/terminal.ts | 4 +- .../trpc/router/usage/account-provisioning.ts | 8 +- .../src/trpc/router/usage/default-account.ts | 64 +++++++++- .../trpc/router/usage/profile-remove.test.ts | 17 ++- .../trpc/router/usage/session-share.test.ts | 17 +++ .../src/trpc/router/usage/session-share.ts | 68 +++++++++-- 14 files changed, 344 insertions(+), 49 deletions(-) create mode 100644 packages/agent-setup/src/default-account-resolver.test.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx index abf04e44306..843383817fc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/usage/components/UsageView/UsageView.tsx @@ -89,7 +89,7 @@ function creditsLine(account: UsageAccount): string | null { } const DEFAULT_TITLE = - "New terminals and agents use this account. Existing ones keep theirs."; + "New agent launches use this account. Relaunch a running agent to switch it."; function AccountCard({ account, @@ -278,7 +278,7 @@ export function UsageView({ hostUrl }: { hostUrl: string | null }) { toast.success( `New ${PROVIDER_LABELS[account.provider]} agents will use ${account.email ?? account.sourceLabel}.`, { - description: "Restart your sessions to use it.", + description: "Relaunch running agents to switch them.", }, ); }, diff --git a/apps/docs/content/docs/usage.mdx b/apps/docs/content/docs/usage.mdx index 33399b69dbd..ae4e8fd1292 100644 --- a/apps/docs/content/docs/usage.mdx +++ b/apps/docs/content/docs/usage.mdx @@ -15,7 +15,7 @@ Each Claude Code and Codex login on your machine gets a card with its official l ## Token usage -Spend is estimated from the session logs already on disk, priced at full API rates — on a subscription your real cost is $0; the number shows what the work would have cost. +Spend is estimated from the session logs already on disk, priced at full API rates — on a subscription it shows what the work would have cost at API prices, not an extra bill. - Breakdowns by model, workspace/project, and day, with drilldowns per workspace and per model. - **Machine resources** (toggle at the top) shows what your workspaces are doing to the machine itself. @@ -27,8 +27,8 @@ Run several subscriptions side by side — a second personal Max account, a work ![Two Claude Code accounts with quota meters, one marked as the default](/images/usage-accounts.png) - **Add account** signs a second subscription into its own profile. Your current login is untouched. -- **Make default** points new terminals and agents at that account. Running sessions keep their current account until restarted. -- Your setup comes along: skills, plugins, MCP servers, and settings are shared across accounts, and there's one conversation history — `--resume` shows the same sessions whichever account is active. -- Only the login itself stays per account. Removing an account deletes its profile and credentials, never your shared setup or history. +- **Make default** points new agent launches at that account — including relaunches in terminals you already have open. A running agent keeps its account until you relaunch it. +- Your setup comes along: skills, plugins, MCP servers, and settings are shared across accounts, and for Claude Code there's one conversation history — `--resume` shows the same sessions whichever account is active. +- Logins, credentials, and account identity stay per account. Removing an account deletes its profile and credentials, never your shared setup or history. -Under the hood, a profile is just a config dir the CLI is pointed at (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`), injected into new terminals and agent launches — so switching works for agents you launch by hand in a Superset terminal too. +Under the hood, a profile is just a config dir the CLI is pointed at (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`). Superset's agent wrappers re-resolve the current default every time the agent starts in a Superset terminal — so switching works for agents you launch by hand too, and a value you export yourself always wins. diff --git a/docs/agent-tooling.md b/docs/agent-tooling.md index 3bb2e533478..49c5c56ba2f 100644 --- a/docs/agent-tooling.md +++ b/docs/agent-tooling.md @@ -36,11 +36,13 @@ description matches the task. ## Provider accounts (multi-login) -The Usage tab can hold several Claude Code / Codex logins and pick which one new terminals and -agents use. A login is just a config dir the CLI is pointed at — `CLAUDE_CONFIG_DIR` / -`CODEX_HOME`, injected at PTY and agent launch by -`packages/host-service/src/trpc/router/usage/default-account.ts`. Superset never touches the -credential stores; the provider CLIs own every login end to end. +The Usage tab can hold several Claude Code / Codex logins and pick which one agents use. A login +is just a config dir the CLI is pointed at — `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, injected at PTY +and agent launch by `packages/host-service/src/trpc/router/usage/default-account.ts`, which also +publishes the selection to `$SUPERSET_HOME_DIR/state/default-*` pointer files. The agent wrappers +re-read those at every launch (`buildDefaultAccountResolver` in agent-setup), so a switch reaches +existing terminals the next time the agent starts; a value the user exported by hand always wins. +Superset never touches the credential stores; the provider CLIs own every login end to end. Because those CLIs read *everything* from their active config dir, a second dir would otherwise mean a second, empty setup. `packages/agent-setup/src/provider-profiles.ts` provisions each diff --git a/packages/agent-setup/src/agent-wrappers-claude-codex-opencode.ts b/packages/agent-setup/src/agent-wrappers-claude-codex-opencode.ts index 35aa5f5a56f..b629a2f3cc7 100644 --- a/packages/agent-setup/src/agent-wrappers-claude-codex-opencode.ts +++ b/packages/agent-setup/src/agent-wrappers-claude-codex-opencode.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { + buildDefaultAccountResolver, buildWrapperScript, createWrapper, getManagedNotifyHookCommand, @@ -251,25 +252,32 @@ export function getOpenCodePluginContent(notifyPath: string): string { } /** - * Pass-through wrapper for Claude. Hooks live in ~/.claude/settings.json - * (createClaudeSettingsJson); the wrapper exists only to forward SUPERSET_* - * env vars into the agent process tree. + * Wrapper for Claude: forwards SUPERSET_* env vars into the agent process + * tree and follows the Usage-tab account switch at launch time. Hooks live + * in ~/.claude/settings.json (createClaudeSettingsJson). */ export function createClaudeWrapper(): void { - const script = buildWrapperScript("claude", `exec "$REAL_BIN" "$@"`, { - agentId: "claude", - }); + const script = buildWrapperScript( + "claude", + buildDefaultAccountResolver( + "CLAUDE_CONFIG_DIR", + "default-claude-config-dir", + ) + `exec "$REAL_BIN" "$@"`, + { agentId: "claude" }, + ); createWrapper("claude", script); } /** - * Creates the Codex wrapper that enables native hooks and session-log signals. + * Creates the Codex wrapper that enables native hooks and session-log signals, + * and follows the Usage-tab account switch at launch time. */ export function createCodexWrapper(): void { const notifyPath = getNotifyScriptPath(); const script = buildWrapperScript( "codex", - buildCodexWrapperExecLine(notifyPath), + buildDefaultAccountResolver("CODEX_HOME", "default-codex-home") + + buildCodexWrapperExecLine(notifyPath), { agentId: "codex" }, ); createWrapper("codex", script); diff --git a/packages/agent-setup/src/agent-wrappers-common.ts b/packages/agent-setup/src/agent-wrappers-common.ts index 52b4749d625..04cc7452d8e 100644 --- a/packages/agent-setup/src/agent-wrappers-common.ts +++ b/packages/agent-setup/src/agent-wrappers-common.ts @@ -105,6 +105,34 @@ function buildRealBinaryResolver(): string { `; } +/** + * Shell block that re-resolves the Usage-tab default account at launch. + * The PTY env is frozen at terminal spawn, so an account switch would + * otherwise reach only brand-new terminals; this re-reads the host's + * pointer file every time the agent starts instead. Superset terminals + * only, and a value the user exported by hand — one that differs from what + * Superset injected at spawn — always wins. A missing pointer file (older + * host build) changes nothing; an empty one means the system default. + */ +export function buildDefaultAccountResolver( + envVar: string, + pointerName: string, +): string { + const pointer = `"$SUPERSET_HOME_DIR/state/${pointerName}"`; + return `if [ -n "$SUPERSET_TERMINAL_ID" ] && [ -n "$SUPERSET_HOME_DIR" ] \\ + && { [ -z "\${${envVar}}" ] || [ "\${${envVar}}" = "\${SUPERSET_DEFAULT_${envVar}}" ]; } \\ + && [ -f ${pointer} ]; then + superset_default_account="$(cat ${pointer} 2>/dev/null)" + if [ -n "$superset_default_account" ] && [ -d "$superset_default_account" ]; then + export ${envVar}="$superset_default_account" + else + unset ${envVar} + fi +fi + +`; +} + function getMissingBinaryMessage(name: string): string { return `Superset: ${name} not found in PATH. Install it and ensure it is on PATH, then retry.`; } diff --git a/packages/agent-setup/src/agent-wrappers.test.ts b/packages/agent-setup/src/agent-wrappers.test.ts index dbed78030a0..04cce83385f 100644 --- a/packages/agent-setup/src/agent-wrappers.test.ts +++ b/packages/agent-setup/src/agent-wrappers.test.ts @@ -173,7 +173,11 @@ describe("agent-wrappers copilot", () => { expect(wrapper).toContain( 'projects={\\"$_superset_workspace_path_toml\\"={trust_level=\\"trusted\\"}}', ); - expect(wrapper).not.toContain("export CODEX_HOME="); + // The Usage-tab default resolver may export CODEX_HOME dynamically from + // the pointer file, but the wrapper must never hardcode a home. + expect(wrapper).toContain("state/default-codex-home"); + expect(wrapper).toContain('export CODEX_HOME="$superset_default_account"'); + expect(wrapper).not.toContain('export CODEX_HOME="$HOME'); expect(wrapper).not.toContain("rollout-*.jsonl"); expect(wrapper).not.toContain("_superset_sessions_dir"); expect(wrapper).not.toContain("$" + "{CODEX_HOME:-$HOME/.codex}"); diff --git a/packages/agent-setup/src/default-account-resolver.test.ts b/packages/agent-setup/src/default-account-resolver.test.ts new file mode 100644 index 00000000000..a8b4e1b4390 --- /dev/null +++ b/packages/agent-setup/src/default-account-resolver.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { buildDefaultAccountResolver } from "./agent-wrappers-common"; + +/** Runs the resolver block under bash and returns the resulting env var. */ +function resolve(env: Record): string { + const script = `${buildDefaultAccountResolver( + "CLAUDE_CONFIG_DIR", + "default-claude-config-dir", + )}printf "%s" "\${CLAUDE_CONFIG_DIR:-}"`; + const cleanEnv: Record = { PATH: process.env.PATH ?? "" }; + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) cleanEnv[key] = value; + } + return execFileSync("bash", ["-c", script], { + env: cleanEnv, + encoding: "utf-8", + }); +} + +function makeHome(pointer: string | null): { + home: string; + profile: string; +} { + const home = mkdtempSync(join(tmpdir(), "superset-resolver-")); + const profile = join(home, "profile"); + mkdirSync(join(home, "state"), { recursive: true }); + mkdirSync(profile); + if (pointer !== null) { + writeFileSync(join(home, "state", "default-claude-config-dir"), pointer); + } + return { home, profile }; +} + +describe("buildDefaultAccountResolver", () => { + it("adopts the pointer in a Superset terminal with no spawn-time value", () => { + const { home, profile } = makeHome(""); + writeFileSync(join(home, "state", "default-claude-config-dir"), profile); + expect( + resolve({ SUPERSET_TERMINAL_ID: "t1", SUPERSET_HOME_DIR: home }), + ).toBe(profile); + }); + + it("re-resolves over a stale Superset-injected value", () => { + const { home, profile } = makeHome(null); + writeFileSync(join(home, "state", "default-claude-config-dir"), profile); + expect( + resolve({ + SUPERSET_TERMINAL_ID: "t1", + SUPERSET_HOME_DIR: home, + CLAUDE_CONFIG_DIR: "/tmp/old-spawn-time-default", + SUPERSET_DEFAULT_CLAUDE_CONFIG_DIR: "/tmp/old-spawn-time-default", + }), + ).toBe(profile); + }); + + it("clears a stale injected value when the pointer says system default", () => { + const { home, profile } = makeHome(""); + expect( + resolve({ + SUPERSET_TERMINAL_ID: "t1", + SUPERSET_HOME_DIR: home, + CLAUDE_CONFIG_DIR: profile, + SUPERSET_DEFAULT_CLAUDE_CONFIG_DIR: profile, + }), + ).toBe(""); + }); + + it("never overrides a value the user exported by hand", () => { + const { home, profile } = makeHome(null); + writeFileSync(join(home, "state", "default-claude-config-dir"), profile); + expect( + resolve({ + SUPERSET_TERMINAL_ID: "t1", + SUPERSET_HOME_DIR: home, + CLAUDE_CONFIG_DIR: "/tmp/user-picked-this", + SUPERSET_DEFAULT_CLAUDE_CONFIG_DIR: profile, + }), + ).toBe("/tmp/user-picked-this"); + }); + + it("does nothing outside Superset terminals", () => { + const { home, profile } = makeHome(null); + writeFileSync(join(home, "state", "default-claude-config-dir"), profile); + expect(resolve({ SUPERSET_HOME_DIR: home })).toBe(""); + }); + + it("does nothing when the pointer file is missing (older host build)", () => { + const { home } = makeHome(null); + expect( + resolve({ + SUPERSET_TERMINAL_ID: "t1", + SUPERSET_HOME_DIR: home, + CLAUDE_CONFIG_DIR: "/tmp/spawn-time", + SUPERSET_DEFAULT_CLAUDE_CONFIG_DIR: "/tmp/spawn-time", + }), + ).toBe("/tmp/spawn-time"); + }); + + it("ignores a pointer at a vanished dir instead of booting signed out", () => { + const { home } = makeHome("/tmp/deleted-profile-dir-that-is-gone"); + expect( + resolve({ SUPERSET_TERMINAL_ID: "t1", SUPERSET_HOME_DIR: home }), + ).toBe(""); + }); +}); diff --git a/packages/agent-setup/src/provider-profiles.ts b/packages/agent-setup/src/provider-profiles.ts index 933e964d366..798e106d948 100644 --- a/packages/agent-setup/src/provider-profiles.ts +++ b/packages/agent-setup/src/provider-profiles.ts @@ -46,23 +46,34 @@ function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +/** Real path when the dir exists, plain resolution otherwise — a symlink + * alias of a protected dir must compare equal to it. */ +function canonical(target: string): string { + try { + return fs.realpathSync(target); + } catch { + return path.resolve(target); + } +} + /** * True for the share's source and the other default homes — provisioning a * default home would link it into itself (discovery excludes them, but a - * CLAUDE_CONFIG_DIR env entry can name one directly). + * CLAUDE_CONFIG_DIR env entry can name one directly, including via symlink). */ function isProtectedTarget( target: string, homeDir: string, defaultDir: string, ): boolean { + const resolved = canonical(target); const protectedDirs = [ homeDir, defaultDir, path.join(homeDir, ".config"), path.join(homeDir, ".config", "claude"), ]; - return protectedDirs.some((dir) => target === path.resolve(dir)); + return protectedDirs.some((dir) => resolved === canonical(dir)); } function logReport(provider: string, report: ProfileProvisionReport): void { @@ -246,7 +257,10 @@ const CODEX_SHARED_DIRS = ["prompts"] as const; */ const CODEX_SHARED_FILES = ["config.toml", "AGENTS.md"] as const; -function defaultCodexHome(homeDir: string): string { +function defaultCodexHome(homeDir: string, homeDirOverridden: boolean): string { + // An overridden homeDir (tests) must win over the ambient CODEX_HOME, or + // the provision would share from the real machine's Codex home. + if (homeDirOverridden) return path.join(homeDir, ".codex"); const fromEnv = process.env.CODEX_HOME?.trim(); return fromEnv ? path.resolve(fromEnv) : path.join(homeDir, ".codex"); } @@ -257,7 +271,7 @@ export async function provisionCodexProfile( options: ProvisionProfileOptions = {}, ): Promise { const homeDir = options.homeDir ?? os.homedir(); - const defaultDir = defaultCodexHome(homeDir); + const defaultDir = defaultCodexHome(homeDir, options.homeDir !== undefined); const target = path.resolve(home); const surfaces: Record = {}; if (isProtectedTarget(target, homeDir, defaultDir)) { diff --git a/packages/host-service/src/terminal/terminal.ts b/packages/host-service/src/terminal/terminal.ts index 24def7edeb5..c091f0e0741 100644 --- a/packages/host-service/src/terminal/terminal.ts +++ b/packages/host-service/src/terminal/terminal.ts @@ -2480,8 +2480,8 @@ export async function createTerminalSessionInternal({ hostAgentHookUrl: getHostAgentHookUrl(), }), // Usage-tab default account: provider CLIs typed or preset-launched in - // this terminal run on the selected login. Baked at spawn — existing - // terminals keep the account they started with. + // this terminal run on the selected login. Baked at spawn as the fast + // path; the agent wrappers re-resolve later switches at launch time. ...resolveDefaultAccountTerminalEnv(db), }; diff --git a/packages/host-service/src/trpc/router/usage/account-provisioning.ts b/packages/host-service/src/trpc/router/usage/account-provisioning.ts index ca8b2a13225..9e1bdf23537 100644 --- a/packages/host-service/src/trpc/router/usage/account-provisioning.ts +++ b/packages/host-service/src/trpc/router/usage/account-provisioning.ts @@ -11,7 +11,10 @@ import { provisionCodexProfile, } from "@superset/agent-setup"; import type { HostDb } from "../../../db/index.ts"; -import { getDefaultAccountSelections } from "./default-account.ts"; +import { + getDefaultAccountSelections, + syncDefaultAccountPointers, +} from "./default-account.ts"; import { shareClaudeSessionState } from "./session-share.ts"; /** @@ -32,6 +35,9 @@ export async function provisionClaudeAccount(configDir: string): Promise { * before the next agent launches on it. */ export async function provisionSelectedAccounts(db: HostDb): Promise { + // Heal the wrapper pointer files first — a build predating them (or a + // crashed switch) leaves agents launching on a stale spawn-time default. + syncDefaultAccountPointers(db); const { claudeConfigDir, codexHome } = getDefaultAccountSelections(db); const targets: Array Promise]> = []; // A pointer at a vanished dir is skipped, not recreated: agent launches diff --git a/packages/host-service/src/trpc/router/usage/default-account.ts b/packages/host-service/src/trpc/router/usage/default-account.ts index ab209f1349d..879a8fdaed0 100644 --- a/packages/host-service/src/trpc/router/usage/default-account.ts +++ b/packages/host-service/src/trpc/router/usage/default-account.ts @@ -5,11 +5,55 @@ * provider CLI itself keeps owning every login end to end. */ -import { existsSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import type { HostDb } from "../../../db/index.ts"; import { hostSettings } from "../../../db/schema.ts"; import type { UsageProvider } from "./types.ts"; +const POINTER_NAMES: Record = { + claude: "default-claude-config-dir", + codex: "default-codex-home", +}; + +/** + * Mirror of agent-setup's resolveSupersetHomeDir, not imported: this module + * sits on the terminal env-resolution path (loaded by node --test) and must + * stay free of the agent-setup surface — see account-provisioning.ts. + */ +function supersetHomeDir(): string { + return process.env.SUPERSET_HOME_DIR?.trim() || join(homedir(), ".superset"); +} + +/** + * Publishes a selection where the agent wrappers can re-read it on every + * launch (buildDefaultAccountResolver in agent-setup), so switching accounts + * reaches existing terminals the next time the agent starts — the PTY env + * alone is frozen at spawn. Empty file = system default. Best-effort: the DB + * stays the source of truth and the wrapper falls back to the spawn-time env. + */ +export function syncDefaultAccountPointer( + provider: UsageProvider, + selection: string | null, +): void { + try { + const dir = join(supersetHomeDir(), "state"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, POINTER_NAMES[provider]), selection ?? ""); + } catch { + // Wrapper keeps using the spawn-time env until the next successful sync. + } +} + +/** Reconciles both pointer files from the DB — run at host boot so files + * from an older build (or a crashed switch) heal. */ +export function syncDefaultAccountPointers(db: HostDb): void { + const selections = getDefaultAccountSelections(db); + syncDefaultAccountPointer("claude", selections.claudeConfigDir); + syncDefaultAccountPointer("codex", selections.codexHome); +} + export interface DefaultAccountSelections { /** CLAUDE_CONFIG_DIR to inject, or null for the system-default login. */ claudeConfigDir: string | null; @@ -40,12 +84,15 @@ export function setDefaultAccountSelection( .values({ id: 1, ...values }) .onConflictDoUpdate({ target: hostSettings.id, set: values }) .run(); + syncDefaultAccountPointer(provider, selection); } /** * Env for a new terminal so provider CLIs typed or launched in it run on the * host-default accounts. Both providers' vars — a shell can run either CLI. - * Baked at PTY spawn; existing terminals keep the account they started with. + * Baked at PTY spawn as the fast path; the agent wrappers re-resolve from the + * pointer files at every launch, so a later switch still reaches this + * terminal when the agent is relaunched. */ export function resolveDefaultAccountTerminalEnv( db: HostDb, @@ -72,14 +119,23 @@ export function resolveDefaultAccountEnv( selections.claudeConfigDir && existsSync(selections.claudeConfigDir) ) { - return { CLAUDE_CONFIG_DIR: selections.claudeConfigDir }; + // The SUPERSET_DEFAULT_* twin marks the value as Superset-injected, so + // the agent wrapper can re-resolve a later switch without ever + // overriding a value the user exported by hand. + return { + CLAUDE_CONFIG_DIR: selections.claudeConfigDir, + SUPERSET_DEFAULT_CLAUDE_CONFIG_DIR: selections.claudeConfigDir, + }; } if ( presetId === "codex" && selections.codexHome && existsSync(selections.codexHome) ) { - return { CODEX_HOME: selections.codexHome }; + return { + CODEX_HOME: selections.codexHome, + SUPERSET_DEFAULT_CODEX_HOME: selections.codexHome, + }; } return {}; } diff --git a/packages/host-service/src/trpc/router/usage/profile-remove.test.ts b/packages/host-service/src/trpc/router/usage/profile-remove.test.ts index c18dd048d19..c240fa8d9d0 100644 --- a/packages/host-service/src/trpc/router/usage/profile-remove.test.ts +++ b/packages/host-service/src/trpc/router/usage/profile-remove.test.ts @@ -79,14 +79,19 @@ describe("removeClaudeProfile with shared-config links", () => { writeFileSync(join(sharedSkills, "redesign", "SKILL.md"), "# redesign"); // Under the home dir so the removal guards accept it. - const profile = join(homedir(), `.claude-remove-test-${process.pid}`); - mkdirSync(profile, { recursive: true }); - symlinkSync(sharedSkills, join(profile, "skills"), "dir"); + const profile = mkdtempSync(join(homedir(), ".claude-remove-test-")); + try { + symlinkSync(sharedSkills, join(profile, "skills"), "dir"); - await removeClaudeProfile(profile); + await removeClaudeProfile(profile); - expect(existsSync(profile)).toBe(false); - expect(existsSync(join(sharedSkills, "redesign", "SKILL.md"))).toBe(true); + expect(existsSync(profile)).toBe(false); + expect(existsSync(join(sharedSkills, "redesign", "SKILL.md"))).toBe( + true, + ); + } finally { + rmSync(profile, { recursive: true, force: true }); + } } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/packages/host-service/src/trpc/router/usage/session-share.test.ts b/packages/host-service/src/trpc/router/usage/session-share.test.ts index 388078e85d1..b486b91d8f6 100644 --- a/packages/host-service/src/trpc/router/usage/session-share.test.ts +++ b/packages/host-service/src/trpc/router/usage/session-share.test.ts @@ -130,6 +130,23 @@ describe("shareClaudeSessionState", () => { expect(readdirSync(profile)).not.toContain("projects.superset-merge"); }); + it("separates records when main's history lacks a trailing newline", () => { + const { profile, main } = makeDirs(); + writeFileSync(join(main, "history.jsonl"), '{"m":1}'); // crash-truncated + writeFileSync(join(profile, "history.jsonl"), '{"p":1}\n'); + shareClaudeSessionState(profile, main); + expect(readFileSync(join(main, "history.jsonl"), "utf-8")).toBe( + '{"m":1}\n{"p":1}\n', + ); + }); + + it("refuses a profile dir that is a symlink alias of the main home", () => { + const { profile, main } = makeDirs(); + const alias = join(profile, "..", "main-alias"); + symlinkSync(main, alias); + expect(shareableProfileDir(alias, main)).toBeNull(); + }); + it("appends the profile's prompt history to main's", () => { const { profile, main } = makeDirs(); writeFileSync(join(main, "history.jsonl"), '{"m":1}\n'); diff --git a/packages/host-service/src/trpc/router/usage/session-share.ts b/packages/host-service/src/trpc/router/usage/session-share.ts index 9593451cfb2..1502f02f55f 100644 --- a/packages/host-service/src/trpc/router/usage/session-share.ts +++ b/packages/host-service/src/trpc/router/usage/session-share.ts @@ -22,12 +22,17 @@ import { appendFileSync, + closeSync, lstatSync, mkdirSync, + openSync, readdirSync, readFileSync, + readSync, + realpathSync, renameSync, rmdirSync, + statSync, symlinkSync, unlinkSync, } from "node:fs"; @@ -52,21 +57,35 @@ const SESSION_DIRS = [ const MERGE_SUFFIX = ".superset-merge"; +/** Real path when the dir exists (a symlink alias of a protected dir must + * compare equal to it), plain resolution otherwise. */ +function canonical(path: string): string { + try { + return realpathSync(path); + } catch { + return resolve(path); + } +} + /** Resolved profile dir, or null when it IS a default home (never share a - * default home into itself, and never operate on `~`). */ + * default home into itself, and never operate on `~`). Compares real paths: + * a profile dir that is itself a symlink to `~/.claude` would otherwise pass + * and get linked into itself. */ export function shareableProfileDir( configDir: string, mainHome: string, ): string | null { - const resolved = resolve(configDir); + const resolved = canonical(configDir); const home = homedir(); - const excluded = new Set([ - home, - join(home, ".claude"), - join(home, ".config"), - join(home, ".config", "claude"), - resolve(mainHome), - ]); + const excluded = new Set( + [ + home, + join(home, ".claude"), + join(home, ".config"), + join(home, ".config", "claude"), + mainHome, + ].map(canonical), + ); return excluded.has(resolved) ? null : resolved; } @@ -141,12 +160,39 @@ function mergeAndLinkSessionDir( moveTreeInto(pending, dst); } +/** Appends line-delimited records, inserting a newline first when the + * target's last record lacks one (a crash-truncated write) — plain + * concatenation would fuse the boundary records into one unparsable line. */ +function appendHistoryRecords(dst: string, content: Buffer): void { + if (content.length === 0) return; + let needsSeparator = false; + try { + const size = statSync(dst).size; + if (size > 0) { + const tail = Buffer.alloc(1); + const fd = openSync(dst, "r"); + try { + readSync(fd, tail, 0, 1, size - 1); + } finally { + closeSync(fd); + } + needsSeparator = tail[0] !== 0x0a; + } + } catch { + // Missing target — appendFileSync creates it; no separator needed. + } + appendFileSync( + dst, + needsSeparator ? Buffer.concat([Buffer.from("\n"), content]) : content, + ); +} + function mergeAndLinkHistory(profile: string, main: string): void { const src = join(profile, "history.jsonl"); const dst = join(main, "history.jsonl"); const pending = `${src}${MERGE_SUFFIX}`; if (lstatOrNull(pending)?.isFile()) { - appendFileSync(dst, readFileSync(pending)); + appendHistoryRecords(dst, readFileSync(pending)); unlinkSync(pending); } const info = lstatOrNull(src); @@ -164,7 +210,7 @@ function mergeAndLinkHistory(profile: string, main: string): void { renameSync(pending, src); return; } - appendFileSync(dst, readFileSync(pending)); + appendHistoryRecords(dst, readFileSync(pending)); unlinkSync(pending); }