From da0ca238a02363829d5dbf0386147be4532420a9 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:47:31 +0000 Subject: [PATCH 01/10] feat: choose what a settings backup carries Replace credential detection in the MCP export with a per-category content selection saved in settingsBackup (includeInstructions, includeAgents, includeSkills, includeGlobalMemory, includeMcp, includeMcpHeaders, includeMcpCommands, includePreferences; includeProjects keeps its opt-in default). One selection governs both directions: collection and export skip unselected categories, and a restore only writes selected ones. MCP header values and stdio commands are published as written when selected, gated by the existing byte-bound secret-scan approval. When deselected they become the existing redaction marker, so a restore keeps this machine's values; the same projection is applied to a checked-out backup at restore time so the restoring machine's selection wins. Literal headers from a backup now restore verbatim; {secret: NAME} references keep the local-only endpoint-match rule. The Backup settings screen replaces the static "Included" list with the checkbox selection (MCP has two indented sub-options). --- .../Settings/Sections/BackupSection.tsx | 175 +++-- src/common/config/schemas/settingsBackup.ts | 69 +- src/node/services/backup/adapters.test.ts | 206 +++--- src/node/services/backup/adapters.ts | 59 +- .../backup/backupService.integration.test.ts | 53 +- .../services/backup/backupService.test.ts | 2 +- src/node/services/backup/backupService.ts | 52 +- src/node/services/backup/payload.test.ts | 635 ++++++++++++++---- src/node/services/backup/payload.ts | 273 ++++++-- tests/ui/BackupSection.test.ts | 50 +- 10 files changed, 1136 insertions(+), 438 deletions(-) diff --git a/src/browser/features/Settings/Sections/BackupSection.tsx b/src/browser/features/Settings/Sections/BackupSection.tsx index 6a8cfef1bb..c79e863250 100644 --- a/src/browser/features/Settings/Sections/BackupSection.tsx +++ b/src/browser/features/Settings/Sections/BackupSection.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { ArchiveRestore, CheckCircle2, CloudUpload, RefreshCw, TriangleAlert } from "lucide-react"; +import { ArchiveRestore, CloudUpload, RefreshCw, TriangleAlert } from "lucide-react"; import { Button } from "@/browser/components/Button/Button"; import { Checkbox } from "@/browser/components/Checkbox/Checkbox"; import { ConfirmationModal } from "@/browser/components/ConfirmationModal/ConfirmationModal"; @@ -11,9 +11,17 @@ import { isEditableElement, KEYBINDS, matchesKeybind, + type Keybind, } from "@/browser/utils/ui/keybinds"; import { getErrorMessage } from "@/common/utils/errors"; import type { SettingsBackupInput } from "@/common/orpc/schemas/backup"; +import { + BACKUP_CONTENT_DEFAULTS, + BACKUP_CONTENT_FLAGS, + resolveBackupContents, + type BackupContentFlag, + type BackupContents, +} from "@/common/config/schemas/settingsBackup"; import { BACKUP_CREDENTIAL_LABELS } from "@/constants/backup"; type BackupRoute = keyof APIClient["backup"]; @@ -49,22 +57,58 @@ const BACKUP_SHORTCUTS = [ type BackupShortcutAction = (typeof BACKUP_SHORTCUTS)[number][0]; type BackupShortcutHandlers = Record void | Promise>; -const INCLUDED_SETTINGS = [ - "Global instructions", - "Agent definitions", - "Agent skills", - "Global memory", - "MCP server configuration", - "Portable preferences", -] as const; +interface BackupContentOption { + flag: BackupContentFlag; + label: string; + description?: string; + /** Rendered indented under this flag and disabled while it is off. */ + parent?: BackupContentFlag; + shortcut?: Keybind; +} -type BackupDraft = SettingsBackupInput; +/** One selection governs both directions: what a push publishes and what a restore writes. */ +const BACKUP_CONTENT_OPTIONS: readonly BackupContentOption[] = [ + { flag: "includeInstructions", label: "Global instructions" }, + { flag: "includeAgents", label: "Agent definitions" }, + { flag: "includeSkills", label: "Agent skills" }, + { flag: "includeGlobalMemory", label: "Global memory" }, + { flag: "includePreferences", label: "Portable preferences" }, + { + flag: "includeMcp", + label: "MCP server configuration", + description: + "URLs are copied as written; one that looks like it carries a credential waits for your review before publishing.", + }, + { + flag: "includeMcpHeaders", + parent: "includeMcp", + label: "HTTP header values", + description: + "Copied as written, so a token in a header travels with the backup. Leave off to keep header values on this device.", + }, + { + flag: "includeMcpCommands", + parent: "includeMcp", + label: "stdio commands", + description: + "Copied as written, so a token in a command line travels with the backup. Leave off to keep commands on this device.", + }, + { + flag: "includeProjects", + label: "Project list & project memories", + description: + "Adds your project list and per-project memories to the backup, and lets a restore reimport them on another machine.", + shortcut: KEYBINDS.SETTINGS_BACKUP_TOGGLE_PROJECTS, + }, +]; + +type BackupDraft = SettingsBackupInput & BackupContents; const DEFAULT_DRAFT: BackupDraft = { repoUrl: "", branch: "main", path: "xum/", - includeProjects: false, + ...BACKUP_CONTENT_DEFAULTS, }; function toDraft(settings: SettingsBackupInput): BackupDraft { @@ -72,7 +116,7 @@ function toDraft(settings: SettingsBackupInput): BackupDraft { repoUrl: settings.repoUrl, branch: settings.branch, path: settings.path, - includeProjects: settings.includeProjects === true, + ...resolveBackupContents(settings), }; } @@ -81,7 +125,7 @@ function draftsEqual(left: BackupDraft, right: BackupDraft): boolean { left.repoUrl === right.repoUrl && left.branch === right.branch && left.path === right.path && - (left.includeProjects === true) === (right.includeProjects === true) + BACKUP_CONTENT_FLAGS.every((flag) => left[flag] === right[flag]) ); } @@ -348,7 +392,7 @@ export function BackupSection() { repoUrl: draft.repoUrl.trim(), branch: draft.branch.trim(), path: draft.path.trim(), - includeProjects: draft.includeProjects === true, + ...resolveBackupContents(draft), }); if (!result.success) { setSaveError(getOperationErrorMessage(result.error)); @@ -631,7 +675,7 @@ export function BackupSection() { }, toggleProjects: () => { if (!busy) { - setDraft((current) => ({ ...current, includeProjects: current.includeProjects !== true })); + setDraft((current) => ({ ...current, includeProjects: !current.includeProjects })); } }, }; @@ -712,30 +756,49 @@ export function BackupSection() { - +
+ + What to back up and restore + + {BACKUP_CONTENT_OPTIONS.map((option) => { + const parentOff = option.parent !== undefined && !draft[option.parent]; + return ( + + ); + })} +

+ Provider key files and dedicated secret files are never backed up. Anything selected + here is copied as written; deselecting it later does not remove earlier copies from the + repository's git history. Inside skills and memory, only documentation is published + automatically; any other file waits for you to review it. +

+