From c6d8b2b70872da766c95e183e878a924b6ed88dc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 22:52:19 +0000 Subject: [PATCH 1/2] feat(browser): add Dia source adapter with discovery identity Register Dia as a Chromium-family source browser (Arc User Data layout, Dia Safe Storage). Classify discovered Dia roots as dia so key lookup does not fall through to Chrome Safe Storage, and move unsupported- browser fixtures off dia now that it is supported. Co-authored-by: Matt Van Horn --- CHANGELOG.md | 4 ++++ examples/source.yaml | 4 ++-- internal/chrome/browser.go | 11 ++++++++++ internal/chrome/browser_test.go | 3 ++- internal/chromepaths/discover.go | 18 +++++++++++++++- internal/chromepaths/discover_test.go | 29 +++++++++++++++++++++++++ internal/cli/doctor_test.go | 2 +- internal/config/config.go | 5 +++-- internal/config/config_test.go | 31 ++++++++++++++++++++++++++- 9 files changed, 99 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8da11e5..c7da128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Dia source-browser support + +Dia (The Browser Company) is a Chromium-family source adapter using the same Safe Storage model as Arc, Brave, and Edge. Set `browser.name: dia` in `source.yaml`. Discovery labels Dia's `User Data` root as `dia` so key lookup uses `Dia Safe Storage` rather than Chrome's. + ### Multi-sink fan-out One source can now push the same cookies and secrets to several sinks. diff --git a/examples/source.yaml b/examples/source.yaml index 4339845..5a02fbd 100644 --- a/examples/source.yaml +++ b/examples/source.yaml @@ -14,11 +14,11 @@ chrome: # Optional source browser adapter. Omit for Google Chrome. Set name/profile # when reading cookies from another Chromium-family source browser. -# Supported: chrome, brave, edge, arc. (atlas is recognized but cannot be +# Supported: chrome, brave, edge, arc, dia. (atlas is recognized but cannot be # decrypted by a third-party tool -- see issue #80; `agentcookie doctor` # reports the gap.) # browser: -# name: brave # chrome | brave | edge | arc +# name: brave # chrome | brave | edge | arc | dia # profile: Default peer: diff --git a/internal/chrome/browser.go b/internal/chrome/browser.go index c22099c..b6c1dd1 100644 --- a/internal/chrome/browser.go +++ b/internal/chrome/browser.go @@ -97,6 +97,15 @@ var browserRegistry = map[string]Browser{ KeychainAccount: "Arc", KeychainService: "Arc Safe Storage", }, + // Dia (The Browser Company) follows the same "User Data" layout as Arc. + // Profile paths verified on disk 2026-08-24; keychain account/service + // follow the standard macOS Chromium-fork convention. + "dia": { + Name: "dia", + SupportDir: []string{"Dia", "User Data"}, + KeychainAccount: "Dia", + KeychainService: "Dia Safe Storage", + }, } // LookupBrowser returns the browser descriptor for name. Empty name defaults @@ -168,6 +177,8 @@ func linuxSupportDir(macDirs []string) []string { return macDirs case "Microsoft Edge": return []string{"microsoft-edge"} + case "Arc", "Dia": + return macDirs // Arc and Dia are macOS-only default: return macDirs } diff --git a/internal/chrome/browser_test.go b/internal/chrome/browser_test.go index c20f165..f278658 100644 --- a/internal/chrome/browser_test.go +++ b/internal/chrome/browser_test.go @@ -56,7 +56,7 @@ func TestLookupBrowserAtlas(t *testing.T) { } func TestLookupBrowserUnknownListsSupportedNames(t *testing.T) { - _, err := LookupBrowser("dia") + _, err := LookupBrowser("vivaldi") if err == nil { t.Fatal("expected unsupported browser error") } @@ -76,6 +76,7 @@ func TestLookupBrowserStandardForks(t *testing.T) { {"brave", []string{"BraveSoftware", "Brave-Browser"}, "Brave", "Brave Safe Storage"}, {"edge", []string{"Microsoft Edge"}, "Microsoft Edge", "Microsoft Edge Safe Storage"}, {"arc", []string{"Arc", "User Data"}, "Arc", "Arc Safe Storage"}, + {"dia", []string{"Dia", "User Data"}, "Dia", "Dia Safe Storage"}, } for _, tc := range cases { b, err := LookupBrowser(tc.name) diff --git a/internal/chromepaths/discover.go b/internal/chromepaths/discover.go index 029507e..df2a8a4 100644 --- a/internal/chromepaths/discover.go +++ b/internal/chromepaths/discover.go @@ -5,6 +5,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strings" ) @@ -99,6 +100,10 @@ func chromeRoots() []string { filepath.Join(appSupport, "Chromium"), filepath.Join(appSupport, "BraveSoftware", "Brave-Browser"), filepath.Join(appSupport, "Microsoft Edge"), + // Dia uses Arc's User Data layout. Scan the user-data-dir itself + // (profiles live under Dia/User Data//), not the Dia + // support dir, or Default/Profile N would not be found. + filepath.Join(appSupport, "Dia", "User Data"), ) case "linux": configDir := filepath.Join(home, ".config") @@ -142,7 +147,7 @@ func osDefaultChromeRoot() string { // browserForRoot returns a browser identifier based on the root path. func browserForRoot(root string) string { - lower := strings.ToLower(root) + lower := strings.ToLower(filepath.ToSlash(root)) switch { case strings.Contains(lower, "brave"): return "brave" @@ -150,11 +155,22 @@ func browserForRoot(root string) string { return "edge" case strings.Contains(lower, "chromium"): return "chromium" + case pathHasComponent(lower, "dia"): + // Path-component match: strings.Contains("media", "dia") is true, so + // a substring check would mislabel any root whose path includes + // "media" as Dia and then look up Dia Safe Storage. + return "dia" default: return "chrome" } } +// pathHasComponent reports whether name is a path element of slashPath. +// slashPath must already use forward slashes (filepath.ToSlash). +func pathHasComponent(slashPath, name string) bool { + return slices.Contains(strings.Split(slashPath, "/"), name) +} + // Discover scans known Chrome user-data-dirs and returns all usable // cookie stores. A store is usable if a Cookies file exists at either // /Cookies or /Network/Cookies. Local State is NOT diff --git a/internal/chromepaths/discover_test.go b/internal/chromepaths/discover_test.go index f232560..c761f61 100644 --- a/internal/chromepaths/discover_test.go +++ b/internal/chromepaths/discover_test.go @@ -515,6 +515,8 @@ func TestBrowserForRoot(t *testing.T) { {"/home/me/.config/microsoft-edge", "edge"}, {"/home/me/chrome-profile", "chrome"}, {"/some/random/path", "chrome"}, // Default + {"/Users/me/Library/Application Support/Dia/User Data", "dia"}, + {"/Users/me/Library/Application Support/some-media-app", "chrome"}, // "media" contains "dia" } for _, tc := range cases { @@ -524,3 +526,30 @@ func TestBrowserForRoot(t *testing.T) { } } } + +// TestDiscoverForConfig_DiaUserDataKeepsDiaIdentity is the #120 discovery +// guard: a Dia User Data root must stay labeled "dia" so key lookup uses +// Dia Safe Storage, not Chrome Safe Storage. +func TestDiscoverForConfig_DiaUserDataKeepsDiaIdentity(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + diaRoot := filepath.Join(home, "Library", "Application Support", "Dia", "User Data") + if err := os.MkdirAll(diaRoot, 0o755); err != nil { + t.Fatal(err) + } + makeProfile(t, diaRoot, "Default", true, false) + + result := DiscoverForConfig(diaRoot) + + found := false + for _, s := range result.Stores { + if s.Profile == "Default" && s.Browser == "dia" { + found = true + break + } + } + if !found { + t.Fatalf("expected dia/Default store from Dia User Data root, got %+v", result.Stores) + } +} diff --git a/internal/cli/doctor_test.go b/internal/cli/doctor_test.go index b8e91cf..fe6409b 100644 --- a/internal/cli/doctor_test.go +++ b/internal/cli/doctor_test.go @@ -705,7 +705,7 @@ func TestCheckSourceAdapter(t *testing.T) { t.Run("unknown browser lists supported names", func(t *testing.T) { cfg := &config.SourceConfig{ Chrome: config.ChromeRef{DBPath: "/tmp/Cookies"}, - Browser: config.BrowserRef{Name: "dia"}, + Browser: config.BrowserRef{Name: "vivaldi"}, } c := checkSourceAdapter(cfg, exists, password, decryptOK) if c.Severity != SeverityFail { diff --git a/internal/config/config.go b/internal/config/config.go index 5562aab..611a6e6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -186,6 +186,7 @@ var sourceBrowserPaths = map[string]browserPathRef{ "brave": {SupportDir: []string{"BraveSoftware", "Brave-Browser"}}, "edge": {SupportDir: []string{"Microsoft Edge"}}, "arc": {SupportDir: []string{"Arc", "User Data"}}, + "dia": {SupportDir: []string{"Dia", "User Data"}}, } // SecurityRef holds transport credentials. SharedSecret is the pre-pairing @@ -437,8 +438,8 @@ func linuxSupportDir(macDirs []string) []string { return []string{"microsoft-edge"} case "com.openai.atlas": return macDirs // Atlas is macOS-only - case "Arc": - return macDirs // Arc is macOS-only + case "Arc", "Dia": + return macDirs // Arc and Dia are macOS-only default: return macDirs } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 40c8a83..774e244 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -74,6 +74,35 @@ security: } } +func TestLoadSourceBrowserDiaDerivesUserDataPath(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "source.yaml", ` +sink: + url: http://example.test:9999/sync +browser: + name: dia +security: + shared_secret: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +`) + cfg, err := LoadSource(dir) + if err != nil { + t.Fatalf("LoadSource: %v", err) + } + if cfg.Browser.Name != "dia" { + t.Errorf("browser name: got %q, want dia", cfg.Browser.Name) + } + home, _ := os.UserHomeDir() + var want string + if runtime.GOOS == "linux" { + want = filepath.Join(home, ".config", "Dia", "User Data", "Default", "Cookies") + } else { + want = filepath.Join(home, "Library", "Application Support", "Dia", "User Data", "Default", "Cookies") + } + if cfg.Chrome.DBPath != want { + t.Errorf("derived DBPath: got %q, want %q", cfg.Chrome.DBPath, want) + } +} + func TestLoadSourceDBPathOverridesBrowserDerivedPath(t *testing.T) { dir := t.TempDir() explicit := filepath.Join(dir, "Custom", "Cookies") @@ -123,7 +152,7 @@ func TestLoadSourceUnknownBrowserFailsWithSupportedNames(t *testing.T) { sink: url: http://example.test:9999/sync browser: - name: dia + name: vivaldi security: shared_secret: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa `) From 2e9e0888ce7568c51bb345ab21e6454301b40a03 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 22:56:33 +0000 Subject: [PATCH 2/2] fix(chromepaths): require Dia/User Data pair for discovery identity A lone "dia" path component mislabeled custom Chrome roots under a folder or user named dia, so key lookup requested Dia Safe Storage and dropped the store. Match consecutive Dia/User Data components only. Co-authored-by: Matt Van Horn --- internal/chromepaths/discover.go | 25 ++++++++++++------- internal/chromepaths/discover_test.go | 36 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/internal/chromepaths/discover.go b/internal/chromepaths/discover.go index df2a8a4..ed2c91b 100644 --- a/internal/chromepaths/discover.go +++ b/internal/chromepaths/discover.go @@ -5,7 +5,6 @@ import ( "path/filepath" "regexp" "runtime" - "slices" "strings" ) @@ -155,20 +154,28 @@ func browserForRoot(root string) string { return "edge" case strings.Contains(lower, "chromium"): return "chromium" - case pathHasComponent(lower, "dia"): - // Path-component match: strings.Contains("media", "dia") is true, so - // a substring check would mislabel any root whose path includes - // "media" as Dia and then look up Dia Safe Storage. + case isDiaUserDataRoot(lower): + // Require the Dia/User Data pair, not a lone "dia" component. + // CHROME_USER_DATA_DIR or cdp.profile_dir under a folder named + // dia (or a user named dia) must stay Chrome so key lookup does + // not request Dia Safe Storage and drop the store. return "dia" default: return "chrome" } } -// pathHasComponent reports whether name is a path element of slashPath. -// slashPath must already use forward slashes (filepath.ToSlash). -func pathHasComponent(slashPath, name string) bool { - return slices.Contains(strings.Split(slashPath, "/"), name) +// isDiaUserDataRoot reports whether slashPath contains consecutive Dia +// support-dir components ("dia" then "user data"). slashPath must already +// use forward slashes and be lowercased. +func isDiaUserDataRoot(slashPath string) bool { + parts := strings.Split(slashPath, "/") + for i := 0; i+1 < len(parts); i++ { + if parts[i] == "dia" && parts[i+1] == "user data" { + return true + } + } + return false } // Discover scans known Chrome user-data-dirs and returns all usable diff --git a/internal/chromepaths/discover_test.go b/internal/chromepaths/discover_test.go index c761f61..addb6b4 100644 --- a/internal/chromepaths/discover_test.go +++ b/internal/chromepaths/discover_test.go @@ -516,7 +516,10 @@ func TestBrowserForRoot(t *testing.T) { {"/home/me/chrome-profile", "chrome"}, {"/some/random/path", "chrome"}, // Default {"/Users/me/Library/Application Support/Dia/User Data", "dia"}, + {"/Users/me/Library/Application Support/Dia/User Data/Default", "dia"}, {"/Users/me/Library/Application Support/some-media-app", "chrome"}, // "media" contains "dia" + {"/Users/dia/Library/Application Support/Google/Chrome", "chrome"}, + {"/tmp/dia/chrome-profile", "chrome"}, } for _, tc := range cases { @@ -553,3 +556,36 @@ func TestDiscoverForConfig_DiaUserDataKeepsDiaIdentity(t *testing.T) { t.Fatalf("expected dia/Default store from Dia User Data root, got %+v", result.Stores) } } + +// TestDiscoverForConfig_CustomRootUnderDiaDirStaysChrome is the inverse of +// the Dia identity guard: a Chrome user-data-dir whose path merely contains +// a "dia" directory (CHROME_USER_DATA_DIR / cdp.profile_dir) must not be +// labeled dia, or key lookup would request Dia Safe Storage and skip the +// store. +func TestDiscoverForConfig_CustomRootUnderDiaDirStaysChrome(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + chromeRoot := filepath.Join(home, "dia", "agent-chrome") + if err := os.MkdirAll(chromeRoot, 0o755); err != nil { + t.Fatal(err) + } + makeProfile(t, chromeRoot, "Default", true, false) + + result := DiscoverForConfig(chromeRoot) + found := false + for _, s := range result.Stores { + if s.CookiesPath == "" { + continue + } + if s.Browser != "chrome" { + t.Errorf("store under %q labeled %q, want chrome", chromeRoot, s.Browser) + } + if s.Profile == "Default" && s.Browser == "chrome" { + found = true + } + } + if !found { + t.Fatalf("expected chrome/Default store under a dia/ parent, got %+v", result.Stores) + } +}