diff --git a/.changeset/fix-global-settings-reset-project-central-db.md b/.changeset/fix-global-settings-reset-project-central-db.md new file mode 100644 index 0000000000..795e6a23f0 --- /dev/null +++ b/.changeset/fix-global-settings-reset-project-central-db.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix global settings (including the global concurrency cap) intermittently resetting to defaults. +category: fix +dev: Several production call sites built `new CentralCore(store.getFusionDir())`, pointing the central/global DB at the project's `.fusion/` instead of `~/.fusion/` and spawning stray per-project central DBs seeded with default global settings that shadowed real global state. Added `TaskStore.getGlobalSettingsDir()`, routed the secrets store plus the secrets/proxy/node/secrets-sync/settings-sync dashboard routes through it, and added a `resolveGlobalDir()` guard that throws on a project-local `.fusion/` dir (parent is a git repo) so the regression can't silently recur. Existing stray DBs were operator-quarantined. diff --git a/packages/core/src/__tests__/global-settings-guard.test.ts b/packages/core/src/__tests__/global-settings-guard.test.ts index 10610d3699..6f0f7f884d 100644 --- a/packages/core/src/__tests__/global-settings-guard.test.ts +++ b/packages/core/src/__tests__/global-settings-guard.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { resolveGlobalDir } from "../global-settings.js"; @@ -68,3 +68,60 @@ describe("resolveGlobalDir() VITEST guard", () => { }); }); }); + +/* +FNXC:GlobalDirGuard 2026-06-25-22:30: +Regression for the "all my global settings reset" bug: production code that passed a project's `.fusion/` dir (e.g. store.getFusionDir()) to CentralCore/global stores spun up stray per-project central DBs seeded with default global settings that shadowed ~/.fusion. resolveGlobalDir() must refuse a project-local `.fusion/` dir (named `.fusion` inside a git repo) while still accepting the real home global dir and arbitrary non-repo custom dirs. Guard is intentionally inert under VITEST, so these tests clear VITEST to exercise it. +*/ +describe("resolveGlobalDir() project-local .fusion guard", () => { + it("throws when handed a project-local .fusion dir inside a git repo", () => { + withVitestEnv(undefined, () => { + withTempHome((homeDir) => { + const projectRoot = join(homeDir, "code", "my-project"); + mkdirSync(join(projectRoot, ".git"), { recursive: true }); + const projectFusionDir = join(projectRoot, ".fusion"); + mkdirSync(projectFusionDir, { recursive: true }); + + expect(() => resolveGlobalDir(projectFusionDir)).toThrow( + /refusing project-local '\.fusion' directory/, + ); + }); + }); + }); + + it("also catches a git-worktree project (.git file, not dir)", () => { + withVitestEnv(undefined, () => { + withTempHome((homeDir) => { + const worktreeRoot = join(homeDir, "worktrees", "feature"); + mkdirSync(worktreeRoot, { recursive: true }); + writeFileSync(join(worktreeRoot, ".git"), "gitdir: /somewhere/.git/worktrees/feature\n"); + const worktreeFusionDir = join(worktreeRoot, ".fusion"); + mkdirSync(worktreeFusionDir, { recursive: true }); + + expect(() => resolveGlobalDir(worktreeFusionDir)).toThrow( + /refusing project-local '\.fusion' directory/, + ); + }); + }); + }); + + it("allows the real home global dir", () => { + withVitestEnv(undefined, () => { + withTempHome((homeDir) => { + const homeGlobal = join(homeDir, ".fusion"); + expect(resolveGlobalDir(homeGlobal)).toBe(homeGlobal); + }); + }); + }); + + it("allows a custom non-repo global dir (no .git parent)", () => { + withVitestEnv(undefined, () => { + withTempHome((homeDir) => { + const customDir = join(homeDir, "custom-global", ".fusion"); + mkdirSync(customDir, { recursive: true }); + // Parent has no `.git`, so it is not a project worktree. + expect(resolveGlobalDir(customDir)).toBe(customDir); + }); + }); + }); +}); diff --git a/packages/core/src/__tests__/store-secrets-store-global-dir.test.ts b/packages/core/src/__tests__/store-secrets-store-global-dir.test.ts new file mode 100644 index 0000000000..2f6cd1c4ab --- /dev/null +++ b/packages/core/src/__tests__/store-secrets-store-global-dir.test.ts @@ -0,0 +1,47 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { TaskStore } from "../store.js"; + +/* +FNXC:GlobalDirGuard 2026-06-25-23:05: +Symptom-based regression for the "all my global settings reset" bug. The root cause was getSecretsStore() (and dashboard routes) constructing CentralCore with `store.getFusionDir()` (the project's `.fusion/`), which created a stray per-project `fusion-central.db` seeded with default global state that shadowed the real global DB. These tests assert the INVARIANT directly: the secrets store's central DB lands in the resolved GLOBAL dir and NOT inside the project `.fusion/` dir, and that getGlobalSettingsDir() is distinct from getFusionDir(). Surface enumeration: this covers the store/secrets surface; the resolveGlobalDir guard surfaces are covered in global-settings-guard.test.ts. +*/ +describe("TaskStore.getSecretsStore() central DB location (global, not project-local)", () => { + let root: string; + let globalDir: string; + let store: TaskStore; + + beforeEach(async () => { + root = mkdtempSync(join(tmpdir(), "fn-secrets-global-dir-")); + globalDir = join(root, ".fusion-global-settings"); + store = new TaskStore(root, globalDir, { inMemoryDb: true }); + await store.init(); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + it("resolves getGlobalSettingsDir() to the global dir, distinct from getFusionDir()", () => { + expect(store.getGlobalSettingsDir()).toBe(globalDir); + expect(store.getFusionDir()).toBe(join(root, ".fusion")); + expect(store.getGlobalSettingsDir()).not.toBe(store.getFusionDir()); + }); + + it("creates the secrets central DB in the global dir and never in the project .fusion/", async () => { + await store.getSecretsStore(); + + // The central DB must live in the resolved global dir... + expect(existsSync(join(globalDir, "fusion-central.db"))).toBe(true); + // ...and must NOT have spawned a stray per-project central DB (the original bug). + expect(existsSync(join(store.getFusionDir(), "fusion-central.db"))).toBe(false); + }); + + it("returns a stable singleton secrets store across calls", async () => { + const a = await store.getSecretsStore(); + const b = await store.getSecretsStore(); + expect(a).toBe(b); + }); +}); diff --git a/packages/core/src/global-settings.ts b/packages/core/src/global-settings.ts index 02a1474c14..3054849753 100644 --- a/packages/core/src/global-settings.ts +++ b/packages/core/src/global-settings.ts @@ -14,9 +14,9 @@ */ import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { mkdir, readFile, writeFile, rename, chmod } from "node:fs/promises"; -import { existsSync, mkdirSync, renameSync } from "node:fs"; +import { existsSync, mkdirSync, realpathSync, renameSync } from "node:fs"; import type { GlobalSettings } from "./types.js"; import { DEFAULT_GLOBAL_SETTINGS } from "./types.js"; import { sanitizeCliAgentsSettings } from "./settings-schema.js"; @@ -90,7 +90,44 @@ export function resolveGlobalDir(dir?: string): string { ); } - if (hasExplicitDir) return dir; + if (hasExplicitDir) { + /* + FNXC:GlobalDirGuard 2026-06-25-22:10: + Production code must never point the central/global store at a project's `.fusion/` directory. Doing so silently spins up a stray per-project central DB seeded with DEFAULT global settings (globalMaxConcurrent=4, empty global secrets, default centralSettings), which then shadows the real `~/.fusion/fusion-central.db` and manifests as "all my global settings reset". Root cause was call sites passing `store.getFusionDir()` instead of the resolved global dir. + Guard heuristic: a project `.fusion` dir is named `.fusion` and lives inside a git repo (its parent has a `.git` dir or worktree file), whereas the home global dir's parent (the home dir) is not a repo. We only flag dirs that differ from the home-resolved global dir, so legitimately-threaded global dirs and test temp dirs are unaffected. Skipped under VITEST (tests pass explicit temp dirs by design). + + FNXC:GlobalDirGuard 2026-06-25-22:55: + The heuristic is intentionally conservative but can't perfectly distinguish a project `.fusion` from a legitimately version-controlled custom global dir (e.g. a dotfiles repo with `~/dotfiles/.fusion` + `.git`). To avoid hard-crashing that rare setup, honor an explicit opt-out env var `FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR=true`. This is not reachable via normal production call sites (they resolve to ~/.fusion); it only matters for operators who deliberately configure a custom global dir inside a repo. + + FNXC:GlobalDirGuard 2026-06-26-06:25: + Order matters and the home-dir comparison must be normalized: + - Run the CHEAP, read-only checks first (basename is `.fusion` AND its parent contains a `.git`). Only if both hold do we call `resolveGlobalDirForHome()` — which can perform a one-time legacy-dir rename — so we never trigger that filesystem side effect on the hot path (every explicit-dir call, e.g. getGlobalSettingsDir() in dashboard routes, previously hit it). + - Compare against the home global dir using normalized real paths (realpathSync when the path exists, else resolve()), so a trailing slash, doubled separator, or symlinked home dir doesn't make the legitimate home global dir look like a foreign project dir and trip the guard. + */ + if (process.env.VITEST !== "true" && process.env.FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR !== "true") { + const isFusionDirInsideRepo = + basename(dir) === ".fusion" && existsSync(join(dirname(dir), ".git")); + if (isFusionDirInsideRepo) { + const homeGlobalDir = resolveGlobalDirForHome(getHomeDir()); + const normalize = (p: string): string => { + try { + return realpathSync.native(p); + } catch { + return resolve(p); + } + }; + if (normalize(dir) !== normalize(homeGlobalDir)) { + throw new Error( + `resolveGlobalDir(): refusing project-local '.fusion' directory '${dir}' for the central/global store. ` + + "This would create a stray per-project central database seeded with default global settings and silently reset them. " + + "Pass the resolved global dir (or omit the argument so it defaults to ~/.fusion); see TaskStore.getGlobalSettingsDir(). " + + "If this really is your intended global dir (e.g. a version-controlled dotfiles repo), set FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR=true to override.", + ); + } + } + } + return dir; + } return resolveGlobalDirForHome(getHomeDir()); } diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 3ea0fcf63a..4b9f4a48fc 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -146,7 +146,7 @@ import { normalizeTaskPriority } from "./task-priority.js"; import { validateBranchGroupBranchName, filterTasksByBranchGroup } from "./branch-assignment.js"; import { allowsAutoMergeProcessing } from "./task-merge.js"; import { canAgentTakeImplementationTaskForExplicitRouting } from "./agent-role-policy.js"; -import { GlobalSettingsStore } from "./global-settings.js"; +import { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js"; import { Database, SCHEMA_VERSION, toJson, toJsonNullable, fromJson } from "./db.js"; import { ArchiveDatabase } from "./archive-db.js"; import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js"; @@ -16693,6 +16693,17 @@ ${stepsSection}`; return this.fusionDir; } + /* + FNXC:GlobalDirGuard 2026-06-25-22:12: + The resolved GLOBAL settings dir. Distinct from getFusionDir() which is this project's `.fusion/`. Any CentralCore/global-store construction MUST use this, never getFusionDir(); passing the project dir spins up a stray per-project central DB that shadows ~/.fusion and silently resets global settings. + + FNXC:GlobalDirGuard 2026-06-25-22:50: + Returns a fully-RESOLVED absolute path (string), not the raw optional field. Resolving here (rather than leaking CentralCore's `undefined → ~/.fusion` default to every caller) makes the contract honest and fires the project-local `.fusion` guard at this call site instead of deferring it to CentralCore construction. Under VITEST `this.globalSettingsDir` is always set to a temp dir, so resolveGlobalDir returns it verbatim and never throws the no-explicit-dir test error. + */ + getGlobalSettingsDir(): string { + return resolveGlobalDir(this.globalSettingsDir); + } + getTasksDir(): string { return this.tasksDir; } @@ -16715,14 +16726,16 @@ ${stepsSection}`; return this.secretsStore; } - const central = new CentralCore(this.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:13: Secrets live in the GLOBAL central DB (~/.fusion), not this project's `.fusion/`. Use the resolved global dir; passing getFusionDir() created a stray per-project central DB and reset global settings. + const central = new CentralCore(this.getGlobalSettingsDir()); await central.init(); this.secretsCentralCore = central; const centralDb = (central as unknown as { db: import("./central-db.js").CentralDatabase | null }).db; if (!centralDb) { throw new Error("Central database unavailable for secrets store"); } - const masterKeyManager = new MasterKeyManager(); + // FNXC:GlobalDirGuard 2026-06-25-23:00: The master key is GLOBAL — pass the resolved global dir explicitly so it co-locates with the global central DB (matching prod ~/.fusion) and so getSecretsStore() is exercisable under tests (a bare new MasterKeyManager() throws under VITEST because resolveGlobalDir() requires an explicit dir there). + const masterKeyManager = new MasterKeyManager({ globalDir: this.getGlobalSettingsDir() }); const masterKeyProvider = () => masterKeyManager.getOrCreateKey(); this.secretsStore = new SecretsStore(this.db, centralDb, masterKeyProvider); return this.secretsStore; diff --git a/packages/dashboard/src/__tests__/browse-directory-routes.test.ts b/packages/dashboard/src/__tests__/browse-directory-routes.test.ts index d74fcbe858..1cccd30a46 100644 --- a/packages/dashboard/src/__tests__/browse-directory-routes.test.ts +++ b/packages/dashboard/src/__tests__/browse-directory-routes.test.ts @@ -40,6 +40,7 @@ vi.mock("@fusion/core", async () => { // Import after mocking import { browseDirectory } from "../../app/api.js"; +import { CentralCore } from "@fusion/core"; function mockFetchResponse( ok: boolean, @@ -71,6 +72,11 @@ class MockStoreForRoutes extends EventEmitter { return "/tmp/fn-944/.fusion"; } + // FNXC:GlobalDirGuard 2026-06-26-06:25: Return a global dir DISTINCT from getFusionDir() so the regression test can assert the route constructs CentralCore with the global dir — a future revert to getFusionDir() then fails the CentralCore-constructor assertion below instead of silently passing. + getGlobalSettingsDir(): string { + return "/tmp/fn-944/.fusion-global"; + } + getDatabase() { return { exec: vi.fn(), @@ -149,6 +155,8 @@ describe("GET /api/browse-directory route handler", () => { expect(mockListNodes).toHaveBeenCalled(); expect(mockClose).toHaveBeenCalled(); expect(globalThis.fetch).not.toHaveBeenCalled(); + // FNXC:GlobalDirGuard 2026-06-26-06:25: The node-aware route must build CentralCore from the GLOBAL dir, never the project `.fusion/`. Asserting the distinct global path makes a revert to getFusionDir() ("/tmp/fn-944/.fusion") fail here. + expect(vi.mocked(CentralCore)).toHaveBeenCalledWith("/tmp/fn-944/.fusion-global"); }); }); diff --git a/packages/dashboard/src/__tests__/proxy-routes.test.ts b/packages/dashboard/src/__tests__/proxy-routes.test.ts index d1f9230a00..007f9b1cbf 100644 --- a/packages/dashboard/src/__tests__/proxy-routes.test.ts +++ b/packages/dashboard/src/__tests__/proxy-routes.test.ts @@ -47,6 +47,11 @@ class MockStore extends EventEmitter { return "/tmp/fn-test/.fusion"; } + // FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change. + getGlobalSettingsDir(): string { + return this.getFusionDir(); + } + getDatabase() { return { exec: vi.fn(), diff --git a/packages/dashboard/src/__tests__/routes-nodes-sync-contract.test.ts b/packages/dashboard/src/__tests__/routes-nodes-sync-contract.test.ts index a07a817e66..dd2236f683 100644 --- a/packages/dashboard/src/__tests__/routes-nodes-sync-contract.test.ts +++ b/packages/dashboard/src/__tests__/routes-nodes-sync-contract.test.ts @@ -88,6 +88,11 @@ class MockStore extends EventEmitter { return "/tmp/fn-4755-test/.fusion"; } + // FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change. + getGlobalSettingsDir(): string { + return this.getFusionDir(); + } + getDatabase() { return { exec: vi.fn(), diff --git a/packages/dashboard/src/__tests__/routes-nodes-sync.test.ts b/packages/dashboard/src/__tests__/routes-nodes-sync.test.ts index d38186bf1d..46e11323f1 100644 --- a/packages/dashboard/src/__tests__/routes-nodes-sync.test.ts +++ b/packages/dashboard/src/__tests__/routes-nodes-sync.test.ts @@ -104,6 +104,11 @@ class MockStore extends EventEmitter { return "/tmp/fn-1821-test/.fusion"; } + // FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change. + getGlobalSettingsDir(): string { + return this.getFusionDir(); + } + getDatabase() { return { exec: vi.fn(), diff --git a/packages/dashboard/src/__tests__/routes-proxy.test.ts b/packages/dashboard/src/__tests__/routes-proxy.test.ts index d54c6e28ef..7047d0512c 100644 --- a/packages/dashboard/src/__tests__/routes-proxy.test.ts +++ b/packages/dashboard/src/__tests__/routes-proxy.test.ts @@ -30,6 +30,11 @@ class MockStore extends EventEmitter { return "/tmp/fn-1806/.fusion"; } + // FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change. + getGlobalSettingsDir(): string { + return this.getFusionDir(); + } + getDatabase() { return { exec: vi.fn(), diff --git a/packages/dashboard/src/routes.ts b/packages/dashboard/src/routes.ts index a6cff4824b..0c6c7a3b96 100644 --- a/packages/dashboard/src/routes.ts +++ b/packages/dashboard/src/routes.ts @@ -4463,7 +4463,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout // Node-aware proxying: route to remote node if nodeId is provided and not local if (nodeId) { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:40: Node-aware proxy lookup uses GLOBAL central state — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); const localNodes = await central.listNodes(); diff --git a/packages/dashboard/src/routes/register-proxy-routes.ts b/packages/dashboard/src/routes/register-proxy-routes.ts index d14435afca..3e1a7400eb 100644 --- a/packages/dashboard/src/routes/register-proxy-routes.ts +++ b/packages/dashboard/src/routes/register-proxy-routes.ts @@ -34,7 +34,8 @@ async function proxyToRemoteNode( const timeoutMs = proxyOptions?.timeoutMs ?? 10_000; const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:40: Node proxy state is GLOBAL — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale. + const central = new CentralCore(store.getGlobalSettingsDir()); try { await central.init(); @@ -187,7 +188,7 @@ export function registerProxyRoutes(router: Router, deps: ProxyRoutesDeps): void const nodeId = req.params.nodeId as string; const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); try { await central.init(); @@ -360,7 +361,7 @@ export function registerProxyRoutes(router: Router, deps: ProxyRoutesDeps): void const remainingPath = Array.isArray(splat) ? splat.join("/") : splat; const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); try { await central.init(); diff --git a/packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts b/packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts index e3716146cc..930ab7d14f 100644 --- a/packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts +++ b/packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts @@ -92,7 +92,8 @@ export const registerSecretsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => { router.post("/secrets/sync-receive", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:40: Inbound secrets sync writes GLOBAL central state — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale. + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); try { // Validate auth @@ -174,7 +175,7 @@ export const registerSecretsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => { router.get("/secrets/sync-export", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); try { // Validate auth diff --git a/packages/dashboard/src/routes/register-secrets-sync-routes.ts b/packages/dashboard/src/routes/register-secrets-sync-routes.ts index 9d9c9ec0cd..dadb30299e 100644 --- a/packages/dashboard/src/routes/register-secrets-sync-routes.ts +++ b/packages/dashboard/src/routes/register-secrets-sync-routes.ts @@ -52,7 +52,8 @@ export const registerSecretsSyncRoutes: ApiRouteRegistrar = (ctx) => { router.post("/nodes/:id/secrets/push", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:40: Secrets-sync node state is GLOBAL — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale. + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); try { const node = await central.getNode(req.params.id); @@ -110,7 +111,7 @@ export const registerSecretsSyncRoutes: ApiRouteRegistrar = (ctx) => { router.post("/nodes/:id/secrets/pull", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); try { const node = await central.getNode(req.params.id); diff --git a/packages/dashboard/src/routes/register-settings-sync-inbound-routes.ts b/packages/dashboard/src/routes/register-settings-sync-inbound-routes.ts index 57a287dc1d..15a021ed4e 100644 --- a/packages/dashboard/src/routes/register-settings-sync-inbound-routes.ts +++ b/packages/dashboard/src/routes/register-settings-sync-inbound-routes.ts @@ -74,7 +74,8 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => { router.post("/settings/sync-receive", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + // FNXC:GlobalDirGuard 2026-06-25-22:20: Inbound settings sync writes GLOBAL central state, so it must use the resolved global dir (~/.fusion). Previously this (and the secrets/proxy/node routes) passed store.getFusionDir() — the project `.fusion/` — which created a stray per-project central DB seeded with default global settings, the root cause of intermittent "all my global settings reset". Mirror this requirement on every CentralCore construction in dashboard routes. + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); // Validate auth - find local node and check apiKey @@ -181,7 +182,7 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => { router.post("/settings/auth-receive", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); // Validate auth @@ -278,7 +279,7 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => { router.get("/settings/auth-export", async (req, res) => { try { const { CentralCore } = await import("@fusion/core"); - const central = new CentralCore(store.getFusionDir()); + const central = new CentralCore(store.getGlobalSettingsDir()); await central.init(); // Validate auth